aboutsummaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2012-08-13 18:28:50 +0300
committerArtur Signell <artur@vaadin.com>2012-08-13 18:34:56 +0300
commit14dd4d0b28c76eb994b181a4570f3adec53342e6 (patch)
treec00784c1c933abe80298dd76ca102f9348bc5c6f /client
parent9bc14b90a3ec265587562f2886ec3da1cd904f44 (diff)
downloadvaadin-framework-14dd4d0b28c76eb994b181a4570f3adec53342e6.tar.gz
vaadin-framework-14dd4d0b28c76eb994b181a4570f3adec53342e6.zip
Moved client files to a client src folder (#9299)
Diffstat (limited to 'client')
-rw-r--r--client/src/com/google/gwt/dom/client/VaadinDOMImplSafari.java26
-rw-r--r--client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml13
-rw-r--r--client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml53
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java676
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java2536
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java378
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/CSSRule.java120
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java30
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java120
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java74
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java56
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java76
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java610
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ComputedStyle.java186
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java97
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java219
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/Console.java32
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java21
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/DateTimeService.java439
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java12
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/EventHelper.java97
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/FastStringSet.java60
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/Focusable.java19
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java372
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java34
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/LayoutManager.java1215
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java50
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java13
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/LocaleService.java148
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java228
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java75
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/NullConsole.java63
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/Paintable.java19
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/RenderInformation.java136
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/RenderSpace.java56
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java540
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ServerConnector.java119
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/SimpleTree.java117
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/StyleConstants.java17
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java253
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java24
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java54
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/UIDL.java551
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/Util.java1181
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VCaption.java595
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java39
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VConsole.java105
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java1004
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java61
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java33
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VTooltip.java352
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java350
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ValueMap.java109
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java11
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java23
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/WidgetMap.java65
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/WidgetSet.java115
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java33
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java19
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java20
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java11
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java27
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java54
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java213
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java286
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java124
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java34
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java38
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java34
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java34
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/Type.java40
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java41
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java36
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java122
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java234
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java407
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java91
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java278
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java49
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java15
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/Action.java58
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java19
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java128
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java51
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java40
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java31
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java43
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/Field.java16
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java80
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java110
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java105
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java184
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/Icon.java44
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java45
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java25
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java40
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java10
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java72
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java8
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java298
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java79
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java8
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java50
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java657
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java56
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java31
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java31
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java280
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java52
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java56
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java555
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java28
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java218
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java134
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java78
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java515
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java45
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java22
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java136
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java419
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java146
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java57
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java241
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java1707
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java166
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java72
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java40
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java22
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java124
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java408
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java111
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java99
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java137
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java49
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java1757
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java185
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java97
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java373
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java340
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java100
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java142
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java20
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java17
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java22
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java45
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java15
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java42
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java21
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java738
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java189
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java12
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java42
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java73
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java17
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java84
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java31
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java45
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java40
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java81
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java64
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java50
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java19
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java39
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java25
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java39
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java44
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java69
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.pngbin0 -> 14779 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.pngbin0 -> 5676 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.pngbin0 -> 9802 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.pngbin0 -> 5769 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.pngbin0 -> 5862 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.pngbin0 -> 6828 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.pngbin0 -> 2113 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.pngbin0 -> 3571 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.pngbin0 -> 3763 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.pngbin0 -> 6140 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.pngbin0 -> 6652 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.pngbin0 -> 9543 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.pngbin0 -> 10067 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java72
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java596
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java69
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java222
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java238
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java207
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java76
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java135
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java366
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java240
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java702
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java69
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java69
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java99
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java25
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java9
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java520
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java86
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java10
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java287
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java93
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java113
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java112
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java520
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java191
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java193
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java1457
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java124
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java125
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java115
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java458
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java94
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java67
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java202
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java171
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java322
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java14
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java241
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java14
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java246
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java188
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java22
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java117
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java375
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java60
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java71
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java73
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java361
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$Strings.properties35
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java464
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gifbin0 -> 104 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gifbin0 -> 900 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gifbin0 -> 954 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gifbin0 -> 96 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gifbin0 -> 147 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gifbin0 -> 173 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.pngbin0 -> 11454 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gifbin0 -> 853 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gifbin0 -> 76 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gifbin0 -> 946 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gifbin0 -> 190 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gifbin0 -> 70 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gifbin0 -> 71 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gifbin0 -> 70 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gifbin0 -> 204 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gifbin0 -> 76 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gifbin0 -> 962 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gifbin0 -> 585 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gifbin0 -> 915 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gifbin0 -> 933 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gifbin0 -> 232 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gifbin0 -> 133 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gifbin0 -> 914 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java431
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java461
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java72
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java530
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java215
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java18
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java772
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java12
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java12
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java19
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java377
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java6917
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java100
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java128
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java1238
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java75
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java189
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java33
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java105
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java106
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java413
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java287
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java2128
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java95
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java830
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java65
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java606
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java61
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java25
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java29
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java307
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java59
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java42
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java920
-rw-r--r--client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java306
-rw-r--r--client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gifbin0 -> 807 bytes
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/AbstractConnectorClassBasedFactoryGenerator.java145
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/AcceptCriteriaFactoryGenerator.java127
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java462
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorStateFactoryGenerator.java29
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorWidgetFactoryGenerator.java29
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/CustomWidgetMapGenerator.java84
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/EagerWidgetMapGenerator.java29
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/GeneratedRpcMethodProviderGenerator.java211
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/LazyWidgetMapGenerator.java23
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyCreatorGenerator.java126
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyGenerator.java142
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerGenerator.java458
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java365
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetMapGenerator.java398
-rw-r--r--client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetSetBuilder.java201
311 files changed, 61103 insertions, 0 deletions
diff --git a/client/src/com/google/gwt/dom/client/VaadinDOMImplSafari.java b/client/src/com/google/gwt/dom/client/VaadinDOMImplSafari.java
new file mode 100644
index 0000000000..6e9cc81f43
--- /dev/null
+++ b/client/src/com/google/gwt/dom/client/VaadinDOMImplSafari.java
@@ -0,0 +1,26 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.google.gwt.dom.client;
+
+/**
+ * Overridden to workaround GWT issue #6194. Remove this when updating to a
+ * newer GWT that fixes the problem (2.3.0 possibly). Must be in this package as
+ * the whole DOMImpl hierarchy is package private and I really did not want to
+ * copy all the parent classes into this one...
+ */
+class VaadinDOMImplSafari extends DOMImplSafari {
+ @Override
+ public int getAbsoluteLeft(Element elem) {
+ // Chrome returns a float in certain cases (at least when zoom != 100%).
+ // The |0 ensures it is converted to an int.
+ return super.getAbsoluteLeft(elem) | 0;
+ }
+
+ @Override
+ public int getAbsoluteTop(Element elem) {
+ // Chrome returns a float in certain cases (at least when zoom != 100%).
+ // The |0 ensures it is converted to an int.
+ return super.getAbsoluteTop(elem) | 0;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml b/client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml
new file mode 100644
index 0000000000..278d92f38f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml
@@ -0,0 +1,13 @@
+<module>
+ <!-- This GWT module defines the Vaadin 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. -->
+
+ <!-- Hint for WidgetSetBuilder not to automatically update the file -->
+ <!-- WS Compiler: manually edited -->
+
+ <inherits name="com.vaadin.Vaadin" />
+
+ <entry-point class="com.vaadin.terminal.gwt.client.ApplicationConfiguration" />
+
+</module>
diff --git a/client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml b/client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml
new file mode 100644
index 0000000000..b5ab61df64
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/VaadinBrowserSpecificOverrides.gwt.xml
@@ -0,0 +1,53 @@
+<module>
+ <!-- This GWT module defines the browser specific overrides used by Vaadin -->
+
+ <!-- Hint for WidgetSetBuilder not to automatically update the file -->
+ <!-- WS Compiler: manually edited -->
+
+ <!-- Fall through to this rule for everything but IE -->
+ <replace-with
+ class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategy">
+ <when-type-is
+ class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategy" />
+ </replace-with>
+
+ <replace-with
+ class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategyIE">
+ <when-type-is
+ class="com.vaadin.terminal.gwt.client.ui.upload.UploadIFrameOnloadStrategy" />
+ <any>
+ <when-property-is name="user.agent" value="ie8" />
+ </any>
+ </replace-with>
+
+ <!-- Fall through to this rule for everything but IE -->
+ <replace-with class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper">
+ <when-type-is class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper" />
+ </replace-with>
+
+ <replace-with class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapperIE">
+ <when-type-is class="com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper" />
+ <any>
+ <when-property-is name="user.agent" value="ie8" />
+ </any>
+ </replace-with>
+
+ <!-- Fall through to this rule for everything but IE -->
+ <replace-with class="com.vaadin.terminal.gwt.client.LayoutManager">
+ <when-type-is class="com.vaadin.terminal.gwt.client.LayoutManager" />
+ </replace-with>
+
+ <replace-with class="com.vaadin.terminal.gwt.client.LayoutManagerIE8">
+ <when-type-is class="com.vaadin.terminal.gwt.client.LayoutManager" />
+ <any>
+ <when-property-is name="user.agent" value="ie8" />
+ </any>
+ </replace-with>
+
+ <!-- Workaround for #6682. Remove when fixed in GWT. -->
+ <replace-with class="com.google.gwt.dom.client.VaadinDOMImplSafari">
+ <when-type-is class="com.google.gwt.dom.client.DOMImpl" />
+ <when-property-is name="user.agent" value="safari" />
+ </replace-with>
+
+</module>
diff --git a/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java b/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
new file mode 100644
index 0000000000..71707e723a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
@@ -0,0 +1,676 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector;
+
+public class ApplicationConfiguration implements EntryPoint {
+
+ public static final String PORTLET_RESOUCE_URL_BASE = "portletAppURLBase";
+
+ /**
+ * Helper class for reading configuration options from the bootstap
+ * javascript
+ *
+ * @since 7.0
+ */
+ private static class JsoConfiguration extends JavaScriptObject {
+ protected JsoConfiguration() {
+ // JSO Constructor
+ }
+
+ /**
+ * Reads a configuration parameter as a string. Please note that the
+ * javascript value of the parameter should also be a string, or else an
+ * undefined exception may be thrown.
+ *
+ * @param name
+ * name of the configuration parameter
+ * @return value of the configuration parameter, or <code>null</code> if
+ * not defined
+ */
+ private native String getConfigString(String name)
+ /*-{
+ var value = this.getConfig(name);
+ if (value === null || value === undefined) {
+ return null;
+ } else {
+ return value +"";
+ }
+ }-*/;
+
+ /**
+ * Reads a configuration parameter as a boolean object. Please note that
+ * the javascript value of the parameter should also be a boolean, or
+ * else an undefined exception may be thrown.
+ *
+ * @param name
+ * name of the configuration parameter
+ * @return boolean value of the configuration paramter, or
+ * <code>null</code> if no value is defined
+ */
+ private native Boolean getConfigBoolean(String name)
+ /*-{
+ var value = this.getConfig(name);
+ if (value === null || value === undefined) {
+ return null;
+ } else {
+ // $entry not needed as function is not exported
+ return @java.lang.Boolean::valueOf(Z)(value);
+ }
+ }-*/;
+
+ /**
+ * Reads a configuration parameter as an integer object. Please note
+ * that the javascript value of the parameter should also be an integer,
+ * or else an undefined exception may be thrown.
+ *
+ * @param name
+ * name of the configuration parameter
+ * @return integer value of the configuration paramter, or
+ * <code>null</code> if no value is defined
+ */
+ private native Integer getConfigInteger(String name)
+ /*-{
+ var value = this.getConfig(name);
+ if (value === null || value === undefined) {
+ return null;
+ } else {
+ // $entry not needed as function is not exported
+ return @java.lang.Integer::valueOf(I)(value);
+ }
+ }-*/;
+
+ /**
+ * Reads a configuration parameter as an {@link ErrorMessage} object.
+ * Please note that the javascript value of the parameter should also be
+ * an object with appropriate fields, or else an undefined exception may
+ * be thrown when calling this method or when calling methods on the
+ * returned object.
+ *
+ * @param name
+ * name of the configuration parameter
+ * @return error message with the given name, or <code>null</code> if no
+ * value is defined
+ */
+ private native ErrorMessage getConfigError(String name)
+ /*-{
+ return this.getConfig(name);
+ }-*/;
+
+ /**
+ * Returns a native javascript object containing version information
+ * from the server.
+ *
+ * @return a javascript object with the version information
+ */
+ private native JavaScriptObject getVersionInfoJSObject()
+ /*-{
+ return this.getConfig("versionInfo");
+ }-*/;
+
+ /**
+ * Gets the version of the Vaadin framework used on the server.
+ *
+ * @return a string with the version
+ *
+ * @see com.vaadin.terminal.gwt.server.AbstractApplicationServlet#VERSION
+ */
+ private native String getVaadinVersion()
+ /*-{
+ return this.getConfig("versionInfo").vaadinVersion;
+ }-*/;
+
+ /**
+ * Gets the version of the application running on the server.
+ *
+ * @return a string with the application version
+ *
+ * @see com.vaadin.Application#getVersion()
+ */
+ private native String getApplicationVersion()
+ /*-{
+ return this.getConfig("versionInfo").applicationVersion;
+ }-*/;
+
+ private native String getUIDL()
+ /*-{
+ return this.getConfig("uidl");
+ }-*/;
+ }
+
+ /**
+ * Wraps a native javascript object containing fields for an error message
+ *
+ * @since 7.0
+ */
+ public static final class ErrorMessage extends JavaScriptObject {
+
+ protected ErrorMessage() {
+ // JSO constructor
+ }
+
+ public final native String getCaption()
+ /*-{
+ return this.caption;
+ }-*/;
+
+ public final native String getMessage()
+ /*-{
+ return this.message;
+ }-*/;
+
+ public final native String getUrl()
+ /*-{
+ return this.url;
+ }-*/;
+ }
+
+ /**
+ * 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 = "9.9.9.INTERNAL-DEBUG-BUILD";
+ } else {
+ VERSION = "@VERSION@";
+ }
+ }
+
+ private static WidgetSet widgetSet = GWT.create(WidgetSet.class);
+
+ private String id;
+ private String themeUri;
+ private String appUri;
+ private int rootId;
+ private boolean standalone;
+ private ErrorMessage communicationError;
+ private ErrorMessage authorizationError;
+ private boolean useDebugIdInDom = true;
+
+ private HashMap<Integer, String> unknownComponents;
+
+ private Class<? extends ServerConnector>[] classes = new Class[1024];
+
+ private boolean browserDetailsSent = false;
+ private boolean widgetsetVersionSent = false;
+
+ static// TODO consider to make this hashmap per application
+ LinkedList<Command> callbacks = new LinkedList<Command>();
+
+ private static int dependenciesLoading;
+
+ private static ArrayList<ApplicationConnection> runningApplications = new ArrayList<ApplicationConnection>();
+
+ private Map<Integer, Integer> componentInheritanceMap = new HashMap<Integer, Integer>();
+ private Map<Integer, String> tagToServerSideClassName = new HashMap<Integer, String>();
+
+ public boolean usePortletURLs() {
+ return getPortletResourceUrl() != null;
+ }
+
+ public String getPortletResourceUrl() {
+ return getJsoConfiguration(id)
+ .getConfigString(PORTLET_RESOUCE_URL_BASE);
+ }
+
+ public String getRootPanelId() {
+ return id;
+ }
+
+ /**
+ * Gets the application base URI. Using this other than as the download
+ * action URI can cause problems in Portlet 2.0 deployments.
+ *
+ * @return application base URI
+ */
+ public String getApplicationUri() {
+ return appUri;
+ }
+
+ public String getThemeName() {
+ String uri = getThemeUri();
+ String themeName = uri.substring(uri.lastIndexOf('/'));
+ themeName = themeName.replaceAll("[^a-zA-Z0-9]", "");
+ return themeName;
+ }
+
+ public String getThemeUri() {
+ return themeUri;
+ }
+
+ public void setAppId(String appId) {
+ id = appId;
+ }
+
+ /**
+ * Gets the initial UIDL from the DOM, if it was provided during the init
+ * process.
+ *
+ * @return
+ */
+ public String getUIDL() {
+ return getJsoConfiguration(id).getUIDL();
+ }
+
+ /**
+ * @return true if the application is served by std. Vaadin servlet and is
+ * considered to be the only or main content of the host page.
+ */
+ public boolean isStandalone() {
+ return standalone;
+ }
+
+ /**
+ * Gets the root if of this application instance. The root id should be
+ * included in every request originating from this instance in order to
+ * associate it with the right Root instance on the server.
+ *
+ * @return the root id
+ */
+ public int getRootId() {
+ return rootId;
+ }
+
+ public JavaScriptObject getVersionInfoJSObject() {
+ return getJsoConfiguration(id).getVersionInfoJSObject();
+ }
+
+ public ErrorMessage getCommunicationError() {
+ return communicationError;
+ }
+
+ public ErrorMessage getAuthorizationError() {
+ return authorizationError;
+ }
+
+ /**
+ * Reads the configuration values defined by the bootstrap javascript.
+ */
+ private void loadFromDOM() {
+ JsoConfiguration jsoConfiguration = getJsoConfiguration(id);
+ appUri = jsoConfiguration.getConfigString("appUri");
+ if (appUri != null && !appUri.endsWith("/")) {
+ appUri += '/';
+ }
+ themeUri = jsoConfiguration.getConfigString("themeUri");
+ rootId = jsoConfiguration.getConfigInteger("rootId").intValue();
+
+ // null -> true
+ useDebugIdInDom = jsoConfiguration.getConfigBoolean("useDebugIdInDom") != Boolean.FALSE;
+
+ // null -> false
+ standalone = jsoConfiguration.getConfigBoolean("standalone") == Boolean.TRUE;
+
+ communicationError = jsoConfiguration.getConfigError("comErrMsg");
+ authorizationError = jsoConfiguration.getConfigError("authErrMsg");
+
+ // boostrap sets initPending to false if it has sent the browser details
+ if (jsoConfiguration.getConfigBoolean("initPending") == Boolean.FALSE) {
+ setBrowserDetailsSent();
+ }
+
+ }
+
+ /**
+ * Starts the application with a given id by reading the configuration
+ * options stored by the bootstrap javascript.
+ *
+ * @param applicationId
+ * id of the application to load, this is also the id of the html
+ * element into which the application should be rendered.
+ */
+ public static void startApplication(final String applicationId) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ ApplicationConfiguration appConf = getConfigFromDOM(applicationId);
+ ApplicationConnection a = GWT
+ .create(ApplicationConnection.class);
+ a.init(widgetSet, appConf);
+ a.start();
+ runningApplications.add(a);
+ }
+ });
+ }
+
+ public static List<ApplicationConnection> getRunningApplications() {
+ return runningApplications;
+ }
+
+ /**
+ * Gets the configuration object for a specific application from the
+ * bootstrap javascript.
+ *
+ * @param appId
+ * the id of the application to get configuration data for
+ * @return a native javascript object containing the configuration data
+ */
+ private native static JsoConfiguration getJsoConfiguration(String appId)
+ /*-{
+ return $wnd.vaadin.getApp(appId);
+ }-*/;
+
+ public static ApplicationConfiguration getConfigFromDOM(String appId) {
+ ApplicationConfiguration conf = new ApplicationConfiguration();
+ conf.setAppId(appId);
+ conf.loadFromDOM();
+ return conf;
+ }
+
+ public String getServletVersion() {
+ return getJsoConfiguration(id).getVaadinVersion();
+ }
+
+ public String getApplicationVersion() {
+ return getJsoConfiguration(id).getApplicationVersion();
+ }
+
+ public boolean useDebugIdInDOM() {
+ return useDebugIdInDom;
+ }
+
+ public Class<? extends ServerConnector> getConnectorClassByEncodedTag(
+ int tag) {
+ try {
+ return classes[tag];
+ } catch (Exception e) {
+ // component was not present in mappings
+ return UnknownComponentConnector.class;
+ }
+ }
+
+ public void addComponentInheritanceInfo(ValueMap valueMap) {
+ JsArrayString keyArray = valueMap.getKeyArray();
+ for (int i = 0; i < keyArray.length(); i++) {
+ String key = keyArray.get(i);
+ int value = valueMap.getInt(key);
+ componentInheritanceMap.put(Integer.parseInt(key), value);
+ }
+ }
+
+ public void addComponentMappings(ValueMap valueMap, WidgetSet widgetSet) {
+ JsArrayString keyArray = valueMap.getKeyArray();
+ for (int i = 0; i < keyArray.length(); i++) {
+ String key = keyArray.get(i).intern();
+ int value = valueMap.getInt(key);
+ tagToServerSideClassName.put(value, key);
+ }
+
+ for (int i = 0; i < keyArray.length(); i++) {
+ String key = keyArray.get(i).intern();
+ int value = valueMap.getInt(key);
+ classes[value] = widgetSet.getConnectorClassByTag(value, this);
+ if (classes[value] == UnknownComponentConnector.class) {
+ if (unknownComponents == null) {
+ unknownComponents = new HashMap<Integer, String>();
+ }
+ unknownComponents.put(value, key);
+ }
+ }
+ }
+
+ public Integer getParentTag(int tag) {
+ return componentInheritanceMap.get(tag);
+ }
+
+ public String getServerSideClassNameForTag(Integer tag) {
+ return tagToServerSideClassName.get(tag);
+ }
+
+ String getUnknownServerClassNameByTag(int tag) {
+ if (unknownComponents != null) {
+ return unknownComponents.get(tag);
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @param c
+ */
+ static void runWhenDependenciesLoaded(Command c) {
+ if (dependenciesLoading == 0) {
+ c.execute();
+ } else {
+ callbacks.add(c);
+ }
+ }
+
+ static void startDependencyLoading() {
+ dependenciesLoading++;
+ }
+
+ static void endDependencyLoading() {
+ dependenciesLoading--;
+ if (dependenciesLoading == 0 && !callbacks.isEmpty()) {
+ for (Command cmd : callbacks) {
+ cmd.execute();
+ }
+ callbacks.clear();
+ } else if (dependenciesLoading == 0 && deferredWidgetLoader != null) {
+ deferredWidgetLoader.trigger();
+ }
+
+ }
+
+ /*
+ * This loop loads widget implementation that should be loaded deferred.
+ */
+ static class DeferredWidgetLoader extends Timer {
+ private static final int FREE_LIMIT = 4;
+ private static final int FREE_CHECK_TIMEOUT = 100;
+
+ int communicationFree = 0;
+ int nextWidgetIndex = 0;
+ private boolean pending;
+
+ public DeferredWidgetLoader() {
+ schedule(5000);
+ }
+
+ public void trigger() {
+ if (!pending) {
+ schedule(FREE_CHECK_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void schedule(int delayMillis) {
+ super.schedule(delayMillis);
+ pending = true;
+ }
+
+ @Override
+ public void run() {
+ pending = false;
+ if (!isBusy()) {
+ Class<? extends ServerConnector> nextType = getNextType();
+ if (nextType == null) {
+ // ensured that all widgets are loaded
+ deferredWidgetLoader = null;
+ } else {
+ communicationFree = 0;
+ widgetSet.loadImplementation(nextType);
+ }
+ } else {
+ schedule(FREE_CHECK_TIMEOUT);
+ }
+ }
+
+ private Class<? extends ServerConnector> getNextType() {
+ Class<? extends ServerConnector>[] deferredLoadedConnectors = widgetSet
+ .getDeferredLoadedConnectors();
+ if (deferredLoadedConnectors.length <= nextWidgetIndex) {
+ return null;
+ } else {
+ return deferredLoadedConnectors[nextWidgetIndex++];
+ }
+ }
+
+ private boolean isBusy() {
+ if (dependenciesLoading > 0) {
+ communicationFree = 0;
+ return true;
+ }
+ for (ApplicationConnection app : runningApplications) {
+ if (app.hasActiveRequest()) {
+ // if an UIDL request or widget loading is active, mark as
+ // busy
+ communicationFree = 0;
+ return true;
+ }
+ }
+ communicationFree++;
+ return communicationFree < FREE_LIMIT;
+ }
+ }
+
+ private static DeferredWidgetLoader deferredWidgetLoader;
+
+ @Override
+ public void onModuleLoad() {
+
+ // Prepare VConsole for debugging
+ if (isDebugMode()) {
+ Console console = GWT.create(Console.class);
+ console.setQuietMode(isQuietDebugMode());
+ console.init();
+ VConsole.setImplementation(console);
+ } else {
+ VConsole.setImplementation((Console) GWT.create(NullConsole.class));
+ }
+ /*
+ * Display some sort of error of exceptions in web mode to debug
+ * console. After this, exceptions are reported to VConsole and possible
+ * GWT hosted mode.
+ */
+ GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+
+ @Override
+ public void onUncaughtException(Throwable e) {
+ /*
+ * Note in case of null console (without ?debug) we eat
+ * exceptions. "a1 is not an object" style errors helps nobody,
+ * especially end user. It does not work tells just as much.
+ */
+ VConsole.getImplementation().error(e);
+ }
+ });
+
+ if (SuperDevMode.enableBasedOnParameter()) {
+ // Do not start any application as super dev mode will refresh the
+ // page once done compiling
+ return;
+ }
+ registerCallback(GWT.getModuleName());
+ deferredWidgetLoader = new DeferredWidgetLoader();
+ }
+
+ /**
+ * Registers that callback that the bootstrap javascript uses to start
+ * applications once the widgetset is loaded and all required information is
+ * available
+ *
+ * @param widgetsetName
+ * the name of this widgetset
+ */
+ public native static void registerCallback(String widgetsetName)
+ /*-{
+ var callbackHandler = $entry(@com.vaadin.terminal.gwt.client.ApplicationConfiguration::startApplication(Ljava/lang/String;));
+ $wnd.vaadin.registerWidgetset(widgetsetName, callbackHandler);
+ }-*/;
+
+ /**
+ * 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 static boolean isDebugMode() {
+ return isDebugAvailable()
+ && Window.Location.getParameter("debug") != null;
+ }
+
+ private native static boolean isDebugAvailable()
+ /*-{
+ if($wnd.vaadin.debug) {
+ return true;
+ } else {
+ return false;
+ }
+ }-*/;
+
+ /**
+ * Checks whether debug logging should be quiet
+ *
+ * @return <code>true</code> if debug logging should be quiet
+ */
+ public static boolean isQuietDebugMode() {
+ String debugParameter = Window.Location.getParameter("debug");
+ return isDebugAvailable() && debugParameter != null
+ && debugParameter.startsWith("q");
+ }
+
+ /**
+ * Checks whether information from the web browser (e.g. uri fragment and
+ * screen size) has been sent to the server.
+ *
+ * @return <code>true</code> if browser information has already been sent
+ *
+ * @see ApplicationConnection#getNativeBrowserDetailsParameters(String)
+ */
+ public boolean isBrowserDetailsSent() {
+ return browserDetailsSent;
+ }
+
+ /**
+ * Registers that the browser details have been sent.
+ * {@link #isBrowserDetailsSent()} will return
+ * <code> after this method has been invoked.
+ */
+ public void setBrowserDetailsSent() {
+ browserDetailsSent = true;
+ }
+
+ /**
+ * Checks whether the widget set version has been sent to the server. It is
+ * sent in the first UIDL request.
+ *
+ * @return <code>true</code> if browser information has already been sent
+ *
+ * @see ApplicationConnection#getNativeBrowserDetailsParameters(String)
+ */
+ public boolean isWidgetsetVersionSent() {
+ return widgetsetVersionSent;
+ }
+
+ /**
+ * Registers that the widget set version has been sent to the server.
+ */
+ public void setWidgetsetVersionSent() {
+ widgetsetVersionSent = true;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
new file mode 100644
index 0000000000..d757bf89a2
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
@@ -0,0 +1,2536 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+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.http.client.URL;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.regexp.shared.MatchResult;
+import com.google.gwt.regexp.shared.RegExp;
+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.Timer;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.shared.communication.UidlValue;
+import com.vaadin.terminal.gwt.client.ApplicationConfiguration.ErrorMessage;
+import com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadEvent;
+import com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener;
+import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.terminal.gwt.client.communication.JsonDecoder;
+import com.vaadin.terminal.gwt.client.communication.JsonEncoder;
+import com.vaadin.terminal.gwt.client.communication.RpcManager;
+import com.vaadin.terminal.gwt.client.communication.SerializerMap;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.communication.Type;
+import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.VContextMenu;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification.HideEvent;
+import com.vaadin.terminal.gwt.client.ui.root.RootConnector;
+import com.vaadin.terminal.gwt.client.ui.window.WindowConnector;
+import com.vaadin.terminal.gwt.server.AbstractCommunicationManager;
+
+/**
+ * This is the client side communication "engine", managing client-server
+ * communication with its server side counterpart
+ * {@link AbstractCommunicationManager}.
+ *
+ * Client-side connectors receive updates from the corresponding server-side
+ * connector (typically component) as state updates or RPC calls. The connector
+ * has the possibility to communicate back with its server side counter part
+ * through RPC calls.
+ *
+ * TODO document better
+ *
+ * Entry point classes (widgetsets) define <code>onModuleLoad()</code>.
+ */
+public class ApplicationConnection {
+ public static final String APP_REQUEST_PATH = "APP/";
+
+ public static final String UIDL_REQUEST_PATH = "UIDL/";
+
+ public static final String APP_PROTOCOL_PREFIX = "app://";
+
+ public static final String V_RESOURCE_PATH = "v-resourcePath";
+
+ public static final String CONNECTOR_PROTOCOL_PREFIX = "connector://";
+
+ public static final String CONNECTOR_RESOURCE_PREFIX = APP_REQUEST_PATH
+ + "CONNECTOR";
+
+ // This indicates the whole page is generated by us (not embedded)
+ public static final String GENERATED_BODY_CLASSNAME = "v-generated-body";
+
+ public static final String MODIFIED_CLASSNAME = "v-modified";
+
+ public static final String DISABLED_CLASSNAME = "v-disabled";
+
+ public static final String REQUIRED_CLASSNAME_EXT = "-required";
+
+ public static final String ERROR_CLASSNAME_EXT = "-error";
+
+ public static final String UPDATE_VARIABLE_INTERFACE = "v";
+ public static final String UPDATE_VARIABLE_METHOD = "v";
+
+ public static final char VAR_BURST_SEPARATOR = '\u001d';
+
+ public static final char VAR_ESCAPE_CHARACTER = '\u001b';
+
+ public static final String UIDL_SECURITY_TOKEN_ID = "Vaadin-Security-Key";
+
+ /**
+ * Name of the parameter used to transmit root ids back and forth
+ */
+ public static final String ROOT_ID_PARAMETER = "rootId";
+
+ /**
+ * @deprecated use UIDL_SECURITY_TOKEN_ID instead
+ */
+ @Deprecated
+ public static final String UIDL_SECURITY_HEADER = UIDL_SECURITY_TOKEN_ID;
+
+ public static final String PARAM_UNLOADBURST = "onunloadburst";
+
+ private static SerializerMap serializerMap;
+
+ /**
+ * A string that, if found in a non-JSON response to a UIDL request, will
+ * cause the browser to refresh the page. If followed by a colon, optional
+ * whitespace, and a URI, causes the browser to synchronously load the URI.
+ *
+ * <p>
+ * This allows, for instance, a servlet filter to redirect the application
+ * to a custom login page when the session expires. For example:
+ * </p>
+ *
+ * <pre>
+ * if (sessionExpired) {
+ * response.setHeader(&quot;Content-Type&quot;, &quot;text/html&quot;);
+ * response.getWriter().write(
+ * myLoginPageHtml + &quot;&lt;!-- Vaadin-Refresh: &quot;
+ * + request.getContextPath() + &quot; --&gt;&quot;);
+ * }
+ * </pre>
+ */
+ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh";
+
+ // will hold the UIDL security key (for XSS protection) once received
+ private String uidlSecurityKey = "init";
+
+ private final HashMap<String, String> resourcesMap = new HashMap<String, String>();
+
+ private ArrayList<MethodInvocation> pendingInvocations = new ArrayList<MethodInvocation>();
+
+ private WidgetSet widgetSet;
+
+ private VContextMenu contextMenu = null;
+
+ private Timer loadTimer;
+ private Timer loadTimer2;
+ private Timer loadTimer3;
+ private Element loadElement;
+
+ private final RootConnector rootConnector;
+
+ protected boolean applicationRunning = false;
+
+ private boolean hasActiveRequest = false;
+
+ protected boolean cssLoaded = false;
+
+ /** Parameters for this application connection loaded from the web-page */
+ private ApplicationConfiguration configuration;
+
+ /** List of pending variable change bursts that must be submitted in order */
+ private final ArrayList<ArrayList<MethodInvocation>> pendingBursts = new ArrayList<ArrayList<MethodInvocation>>();
+
+ /** Timer for automatic refirect to SessionExpiredURL */
+ private Timer redirectTimer;
+
+ /** redirectTimer scheduling interval in seconds */
+ private int sessionExpirationInterval;
+
+ private ArrayList<Widget> componentCaptionSizeChanges = new ArrayList<Widget>();
+
+ private Date requestStartTime;
+
+ private boolean validatingLayouts = false;
+
+ private Set<ComponentConnector> zeroWidthComponents = null;
+
+ private Set<ComponentConnector> zeroHeightComponents = null;
+
+ private final LayoutManager layoutManager;
+
+ private final RpcManager rpcManager;
+
+ public static class MultiStepDuration extends Duration {
+ private int previousStep = elapsedMillis();
+
+ public void logDuration(String message) {
+ logDuration(message, 0);
+ }
+
+ public void logDuration(String message, int minDuration) {
+ int currentTime = elapsedMillis();
+ int stepDuration = currentTime - previousStep;
+ if (stepDuration >= minDuration) {
+ VConsole.log(message + ": " + stepDuration + " ms");
+ }
+ previousStep = currentTime;
+ }
+ }
+
+ public ApplicationConnection() {
+ rootConnector = GWT.create(RootConnector.class);
+ rpcManager = GWT.create(RpcManager.class);
+ layoutManager = GWT.create(LayoutManager.class);
+ layoutManager.setConnection(this);
+ serializerMap = GWT.create(SerializerMap.class);
+ }
+
+ public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) {
+ VConsole.log("Starting application " + cnf.getRootPanelId());
+
+ VConsole.log("Vaadin application servlet version: "
+ + cnf.getServletVersion());
+ VConsole.log("Application version: " + cnf.getApplicationVersion());
+
+ if (!cnf.getServletVersion().equals(ApplicationConfiguration.VERSION)) {
+ VConsole.error("Warning: your widget set seems to be built with a different "
+ + "version than the one used on server. Unexpected "
+ + "behavior may occur.");
+ }
+
+ this.widgetSet = widgetSet;
+ configuration = cnf;
+
+ ComponentLocator componentLocator = new ComponentLocator(this);
+
+ String appRootPanelName = cnf.getRootPanelId();
+ // remove the end (window name) of autogenerated rootpanel id
+ appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", "");
+
+ initializeTestbenchHooks(componentLocator, appRootPanelName);
+
+ initializeClientHooks();
+
+ rootConnector.init(cnf.getRootPanelId(), this);
+ 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.
+ *
+ */
+ public void start() {
+ String jsonText = configuration.getUIDL();
+ if (jsonText == null) {
+ // inital UIDL not in DOM, request later
+ repaintAll();
+ } else {
+ // Update counter so TestBench knows something is still going on
+ hasActiveRequest = true;
+
+ // initial UIDL provided in DOM, continue as if returned by request
+ handleJSONText(jsonText, -1);
+ }
+ }
+
+ private native void initializeTestbenchHooks(
+ ComponentLocator componentLocator, String TTAppId)
+ /*-{
+ var ap = this;
+ var client = {};
+ client.isActive = $entry(function() {
+ return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()()
+ || ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::isExecutingDeferredCommands()();
+ });
+ var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()();
+ if (vi) {
+ client.getVersionInfo = function() {
+ return vi;
+ }
+ }
+
+ client.getProfilingData = $entry(function() {
+ var pd = [
+ ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::lastProcessingTime,
+ ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::totalProcessingTime
+ ];
+ pd = pd.concat(ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::serverTimingInfo);
+ return pd;
+ });
+
+ client.getElementByPath = $entry(function(id) {
+ return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
+ });
+ client.getPathForElement = $entry(function(element) {
+ return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
+ });
+
+ $wnd.vaadin.clients[TTAppId] = client;
+ }-*/;
+
+ /**
+ * Helper for tt initialization
+ */
+ private JavaScriptObject getVersionInfo() {
+ return configuration.getVersionInfoJSObject();
+ }
+
+ /**
+ * Publishes a JavaScript API for mash-up applications.
+ * <ul>
+ * <li><code>vaadin.forceSync()</code> sends pending variable changes, in
+ * effect synchronizing the server and client state. This is done for all
+ * applications on host page.</li>
+ * <li><code>vaadin.postRequestHooks</code> is a map of functions which gets
+ * called after each XHR made by vaadin application. Note, that it is
+ * attaching js functions responsibility to create the variable like this:
+ *
+ * <code><pre>
+ * if(!vaadin.postRequestHooks) {vaadin.postRequestHooks = new Object();}
+ * postRequestHooks.myHook = function(appId) {
+ * if(appId == "MyAppOfInterest") {
+ * // do the staff you need on xhr activity
+ * }
+ * }
+ * </pre></code> First parameter passed to these functions is the identifier
+ * of Vaadin application that made the request.
+ * </ul>
+ *
+ * TODO make this multi-app aware
+ */
+ private native void initializeClientHooks()
+ /*-{
+ var app = this;
+ var oldSync;
+ if ($wnd.vaadin.forceSync) {
+ oldSync = $wnd.vaadin.forceSync;
+ }
+ $wnd.vaadin.forceSync = $entry(function() {
+ if (oldSync) {
+ oldSync();
+ }
+ app.@com.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()();
+ });
+ var oldForceLayout;
+ if ($wnd.vaadin.forceLayout) {
+ oldForceLayout = $wnd.vaadin.forceLayout;
+ }
+ $wnd.vaadin.forceLayout = $entry(function() {
+ if (oldForceLayout) {
+ oldForceLayout();
+ }
+ app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()();
+ });
+ }-*/;
+
+ /**
+ * Runs possibly registered client side post request hooks. This is expected
+ * to be run after each uidl request made by Vaadin application.
+ *
+ * @param appId
+ */
+ private static native void runPostRequestHooks(String appId)
+ /*-{
+ if ($wnd.vaadin.postRequestHooks) {
+ for ( var hook in $wnd.vaadin.postRequestHooks) {
+ if (typeof ($wnd.vaadin.postRequestHooks[hook]) == "function") {
+ try {
+ $wnd.vaadin.postRequestHooks[hook](appId);
+ } catch (e) {
+ }
+ }
+ }
+ }
+ }-*/;
+
+ /**
+ * If on Liferay and logged in, ask the client side session management
+ * JavaScript to extend the session duration.
+ *
+ * Otherwise, Liferay client side JavaScript will explicitly expire the
+ * session even though the server side considers the session to be active.
+ * See ticket #8305 for more information.
+ */
+ protected native void extendLiferaySession()
+ /*-{
+ if ($wnd.Liferay && $wnd.Liferay.Session) {
+ $wnd.Liferay.Session.extend();
+ // if the extend banner is visible, hide it
+ if ($wnd.Liferay.Session.banner) {
+ $wnd.Liferay.Session.banner.remove();
+ }
+ }
+ }-*/;
+
+ /**
+ * Get the active Console for writing debug messages. May return an actual
+ * logging console, or the NullConsole if debugging is not turned on.
+ *
+ * @deprecated Developers should use {@link VConsole} since 6.4.5
+ *
+ * @return the active Console
+ */
+ @Deprecated
+ public static Console getConsole() {
+ return VConsole.getImplementation();
+ }
+
+ /**
+ * Checks if client side is in debug mode. Practically this is invoked by
+ * adding ?debug parameter to URI.
+ *
+ * @deprecated use ApplicationConfiguration isDebugMode instead.
+ *
+ * @return true if client side is currently been debugged
+ */
+ @Deprecated
+ public static boolean isDebugMode() {
+ return ApplicationConfiguration.isDebugMode();
+ }
+
+ /**
+ * Gets the application base URI. Using this other than as the download
+ * action URI can cause problems in Portlet 2.0 deployments.
+ *
+ * @return application base URI
+ */
+ public String getAppUri() {
+ return configuration.getApplicationUri();
+ };
+
+ /**
+ * Indicates whether or not there are currently active UIDL requests. Used
+ * internally to sequence requests properly, seldom needed in Widgets.
+ *
+ * @return true if there are active requests
+ */
+ public boolean hasActiveRequest() {
+ return hasActiveRequest;
+ }
+
+ private String getRepaintAllParameters() {
+ // collect some client side data that will be sent to server on
+ // initial uidl request
+ String nativeBootstrapParameters = getNativeBrowserDetailsParameters(getConfiguration()
+ .getRootPanelId());
+ // TODO figure out how client and view size could be used better on
+ // server. screen size can be accessed via Browser object, but other
+ // values currently only via transaction listener.
+ String parameters = "repaintAll=1&" + nativeBootstrapParameters;
+ return parameters;
+ }
+
+ /**
+ * Gets the browser detail parameters that are sent by the bootstrap
+ * javascript for two-request initialization.
+ *
+ * @param parentElementId
+ * @return
+ */
+ private static native String getNativeBrowserDetailsParameters(
+ String parentElementId)
+ /*-{
+ return $wnd.vaadin.getBrowserDetailsParameters(parentElementId);
+ }-*/;
+
+ protected void repaintAll() {
+ String repainAllParameters = getRepaintAllParameters();
+ makeUidlRequest("", repainAllParameters, false);
+ }
+
+ /**
+ * Requests an analyze of layouts, to find inconsistencies. Exclusively used
+ * for debugging during development.
+ */
+ public void analyzeLayouts() {
+ String params = getRepaintAllParameters() + "&analyzeLayouts=1";
+ makeUidlRequest("", params, false);
+ }
+
+ /**
+ * Sends a request to the server to print details to console that will help
+ * developer to locate component in the source code.
+ *
+ * @param componentConnector
+ */
+ void highlightComponent(ComponentConnector componentConnector) {
+ String params = getRepaintAllParameters() + "&highlightComponent="
+ + componentConnector.getConnectorId();
+ makeUidlRequest("", params, false);
+ }
+
+ /**
+ * Makes an UIDL request to the server.
+ *
+ * @param requestData
+ * Data that is passed to the server.
+ * @param extraParams
+ * Parameters that are added as GET parameters to the url.
+ * Contains key=value pairs joined by & characters or is empty if
+ * no parameters should be added. Should not start with any
+ * special character.
+ * @param forceSync
+ * true if the request should be synchronous, false otherwise
+ */
+ protected void makeUidlRequest(final String requestData,
+ final String extraParams, final boolean forceSync) {
+ startRequest();
+ // Security: double cookie submission pattern
+ final String payload = uidlSecurityKey + VAR_BURST_SEPARATOR
+ + requestData;
+ VConsole.log("Making UIDL Request with params: " + payload);
+ String uri = translateVaadinUri(APP_PROTOCOL_PREFIX + UIDL_REQUEST_PATH);
+
+ if (extraParams != null && extraParams.length() > 0) {
+ uri = addGetParameters(uri, extraParams);
+ }
+ uri = addGetParameters(uri,
+ ROOT_ID_PARAMETER + "=" + configuration.getRootId());
+
+ doUidlRequest(uri, payload, forceSync);
+
+ }
+
+ /**
+ * Sends an asynchronous or synchronous UIDL request to the server using the
+ * given URI.
+ *
+ * @param uri
+ * The URI to use for the request. May includes GET parameters
+ * @param payload
+ * The contents of the request to send
+ * @param synchronous
+ * true if the request should be synchronous, false otherwise
+ */
+ protected void doUidlRequest(final String uri, final String payload,
+ final boolean synchronous) {
+ if (!synchronous) {
+ RequestCallback requestCallback = new RequestCallback() {
+ @Override
+ public void onError(Request request, Throwable exception) {
+ showCommunicationError(exception.getMessage(), -1);
+ endRequest();
+ }
+
+ @Override
+ public void onResponseReceived(Request request,
+ Response response) {
+ VConsole.log("Server visit took "
+ + String.valueOf((new Date()).getTime()
+ - requestStartTime.getTime()) + "ms");
+
+ int statusCode = response.getStatusCode();
+
+ switch (statusCode) {
+ case 0:
+ showCommunicationError(
+ "Invalid status code 0 (server down?)",
+ statusCode);
+ endRequest();
+ return;
+
+ case 401:
+ /*
+ * Authorization has failed. Could be that the session
+ * has timed out and the container is redirecting to a
+ * login page.
+ */
+ showAuthenticationError("");
+ endRequest();
+ return;
+
+ case 503:
+ /*
+ * We'll assume msec instead of the usual seconds. If
+ * there's no Retry-After header, handle the error like
+ * a 500, as per RFC 2616 section 10.5.4.
+ */
+ String delay = response.getHeader("Retry-After");
+ if (delay != null) {
+ VConsole.log("503, retrying in " + delay + "msec");
+ (new Timer() {
+ @Override
+ public void run() {
+ doUidlRequest(uri, payload, synchronous);
+ }
+ }).schedule(Integer.parseInt(delay));
+ return;
+ }
+ }
+
+ if ((statusCode / 100) == 4) {
+ // Handle all 4xx errors the same way as (they are
+ // all permanent errors)
+ showCommunicationError(
+ "UIDL could not be read from server. Check servlets mappings. Error code: "
+ + statusCode, statusCode);
+ endRequest();
+ return;
+ } else if ((statusCode / 100) == 5) {
+ // Something's wrong on the server, there's nothing the
+ // client can do except maybe try again.
+ showCommunicationError("Server error. Error code: "
+ + statusCode, statusCode);
+ endRequest();
+ return;
+ }
+
+ String contentType = response.getHeader("Content-Type");
+ if (contentType == null
+ || !contentType.startsWith("application/json")) {
+ /*
+ * A servlet filter or equivalent may have intercepted
+ * the request and served non-UIDL content (for
+ * instance, a login page if the session has expired.)
+ * If the response contains a magic substring, do a
+ * synchronous refresh. See #8241.
+ */
+ MatchResult refreshToken = RegExp.compile(
+ UIDL_REFRESH_TOKEN + "(:\\s*(.*?))?(\\s|$)")
+ .exec(response.getText());
+ if (refreshToken != null) {
+ redirect(refreshToken.getGroup(2));
+ return;
+ }
+ }
+
+ // for(;;);[realjson]
+ final String jsonText = response.getText().substring(9,
+ response.getText().length() - 1);
+ handleJSONText(jsonText, statusCode);
+ }
+
+ };
+ try {
+ doAsyncUIDLRequest(uri, payload, requestCallback);
+ } catch (RequestException e) {
+ VConsole.error(e);
+ endRequest();
+ }
+ } else {
+ // Synchronized call, discarded response (leaving the page)
+ SynchronousXHR syncXHR = (SynchronousXHR) SynchronousXHR.create();
+ syncXHR.synchronousPost(uri + "&" + PARAM_UNLOADBURST + "=1",
+ payload);
+ /*
+ * Although we are in theory leaving the page, the page may still
+ * stay open. End request properly here too. See #3289
+ */
+ endRequest();
+ }
+
+ }
+
+ /**
+ * Handles received UIDL JSON text, parsing it, and passing it on to the
+ * appropriate handlers, while logging timiing information.
+ *
+ * @param jsonText
+ * @param statusCode
+ */
+ private void handleJSONText(String jsonText, int statusCode) {
+ final Date start = new Date();
+ final ValueMap json;
+ try {
+ json = parseJSONResponse(jsonText);
+ } catch (final Exception e) {
+ endRequest();
+ showCommunicationError(e.getMessage() + " - Original JSON-text:"
+ + jsonText, statusCode);
+ return;
+ }
+
+ VConsole.log("JSON parsing took "
+ + (new Date().getTime() - start.getTime()) + "ms");
+ if (applicationRunning) {
+ handleReceivedJSONMessage(start, jsonText, json);
+ } else {
+ applicationRunning = true;
+ handleWhenCSSLoaded(jsonText, json);
+ }
+ }
+
+ /**
+ * Sends an asynchronous UIDL request to the server using the given URI.
+ *
+ * @param uri
+ * The URI to use for the request. May includes GET parameters
+ * @param payload
+ * The contents of the request to send
+ * @param requestCallback
+ * The handler for the response
+ * @throws RequestException
+ * if the request could not be sent
+ */
+ protected void doAsyncUIDLRequest(String uri, String payload,
+ RequestCallback requestCallback) throws RequestException {
+ RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
+ // TODO enable timeout
+ // rb.setTimeoutMillis(timeoutMillis);
+ rb.setHeader("Content-Type", "text/plain;charset=utf-8");
+ rb.setRequestData(payload);
+ rb.setCallback(requestCallback);
+
+ rb.send();
+ }
+
+ int cssWaits = 0;
+
+ /**
+ * Holds the time spent rendering the last request
+ */
+ protected int lastProcessingTime;
+
+ /**
+ * Holds the total time spent rendering requests during the lifetime of the
+ * session.
+ */
+ protected int totalProcessingTime;
+
+ /**
+ * Holds the timing information from the server-side. How much time was
+ * spent servicing the last request and how much time has been spent
+ * servicing the session so far. These values are always one request behind,
+ * since they cannot be measured before the request is finished.
+ */
+ private ValueMap serverTimingInfo;
+
+ static final int MAX_CSS_WAITS = 100;
+
+ protected void handleWhenCSSLoaded(final String jsonText,
+ final ValueMap json) {
+ if (!isCSSLoaded() && cssWaits < MAX_CSS_WAITS) {
+ (new Timer() {
+ @Override
+ public void run() {
+ handleWhenCSSLoaded(jsonText, json);
+ }
+ }).schedule(50);
+ VConsole.log("Assuming CSS loading is not complete, "
+ + "postponing render phase. "
+ + "(.v-loading-indicator height == 0)");
+ cssWaits++;
+ } else {
+ cssLoaded = true;
+ handleReceivedJSONMessage(new Date(), jsonText, json);
+ if (cssWaits >= MAX_CSS_WAITS) {
+ VConsole.error("CSS files may have not loaded properly.");
+ }
+ }
+ }
+
+ /**
+ * Checks whether or not the CSS is loaded. By default checks the size of
+ * the loading indicator element.
+ *
+ * @return
+ */
+ protected boolean isCSSLoaded() {
+ return cssLoaded
+ || DOM.getElementPropertyInt(loadElement, "offsetHeight") != 0;
+ }
+
+ /**
+ * Shows the communication error notification.
+ *
+ * @param details
+ * Optional details for debugging.
+ * @param statusCode
+ * The status code returned for the request
+ *
+ */
+ protected void showCommunicationError(String details, int statusCode) {
+ VConsole.error("Communication error: " + details);
+ ErrorMessage communicationError = configuration.getCommunicationError();
+ showError(details, communicationError.getCaption(),
+ communicationError.getMessage(), communicationError.getUrl());
+ }
+
+ /**
+ * Shows the authentication error notification.
+ *
+ * @param details
+ * Optional details for debugging.
+ */
+ protected void showAuthenticationError(String details) {
+ VConsole.error("Authentication error: " + details);
+ ErrorMessage authorizationError = configuration.getAuthorizationError();
+ showError(details, authorizationError.getCaption(),
+ authorizationError.getMessage(), authorizationError.getUrl());
+ }
+
+ /**
+ * Shows the error notification.
+ *
+ * @param details
+ * Optional details for debugging.
+ */
+ private void showError(String details, String caption, String message,
+ String url) {
+
+ StringBuilder html = new StringBuilder();
+ if (caption != null) {
+ html.append("<h1>");
+ html.append(caption);
+ html.append("</h1>");
+ }
+ if (message != null) {
+ html.append("<p>");
+ html.append(message);
+ html.append("</p>");
+ }
+
+ if (html.length() > 0) {
+
+ // Add error description
+ html.append("<br/><p><I style=\"font-size:0.7em\">");
+ html.append(details);
+ html.append("</I></p>");
+
+ VNotification n = VNotification.createNotification(1000 * 60 * 45);
+ n.addEventListener(new NotificationRedirect(url));
+ n.show(html.toString(), VNotification.CENTERED_TOP,
+ VNotification.STYLE_SYSTEM);
+ } else {
+ redirect(url);
+ }
+ }
+
+ protected void startRequest() {
+ if (hasActiveRequest) {
+ VConsole.error("Trying to start a new request while another is active");
+ }
+ hasActiveRequest = true;
+ 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);
+ }
+
+ protected void endRequest() {
+ if (!hasActiveRequest) {
+ VConsole.error("No active request");
+ }
+ // After checkForPendingVariableBursts() there may be a new active
+ // request, so we must set hasActiveRequest to false before, not after,
+ // the call. Active requests used to be tracked with an integer counter,
+ // so setting it after used to work but not with the #8505 changes.
+ hasActiveRequest = false;
+ if (applicationRunning) {
+ checkForPendingVariableBursts();
+ runPostRequestHooks(configuration.getRootPanelId());
+ }
+ // deferring to avoid flickering
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ if (!hasActiveRequest()) {
+ hideLoadingIndicator();
+
+ // If on Liferay and session expiration management is in
+ // use, extend session duration on each request.
+ // Doing it here rather than before the request to improve
+ // responsiveness.
+ // Postponed until the end of the next request if other
+ // requests still pending.
+ extendLiferaySession();
+ }
+ }
+ });
+ }
+
+ /**
+ * 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(pendingInvocations);
+ if (pendingBursts.size() > 0) {
+ for (Iterator<ArrayList<MethodInvocation>> iterator = pendingBursts
+ .iterator(); iterator.hasNext();) {
+ cleanVariableBurst(iterator.next());
+ }
+ ArrayList<MethodInvocation> nextBurst = pendingBursts.get(0);
+ pendingBursts.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(ArrayList<MethodInvocation> variableBurst) {
+ for (int i = 1; i < variableBurst.size(); i++) {
+ String id = variableBurst.get(i).getConnectorId();
+ if (!getConnectorMap().hasConnector(id)
+ && !getConnectorMap().isDragAndDropPaintable(id)) {
+ // variable owner does not exist anymore
+ variableBurst.remove(i);
+ VConsole.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(rootConnector.getWidget().getElement(), loadElement);
+ VConsole.log("inserting load indicator");
+ }
+ DOM.setElementProperty(loadElement, "className", "v-loading-indicator");
+ DOM.setStyleAttribute(loadElement, "display", "block");
+ // Initialize other timers
+ loadTimer2 = new Timer() {
+ @Override
+ public void run() {
+ DOM.setElementProperty(loadElement, "className",
+ "v-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",
+ "v-loading-indicator-wait");
+ }
+ };
+ // Third one kicks in at 5000ms from request start
+ loadTimer3.schedule(4700);
+ }
+
+ private void hideLoadingIndicator() {
+ if (loadTimer != null) {
+ loadTimer.cancel();
+ loadTimer = null;
+ }
+ if (loadTimer2 != null) {
+ loadTimer2.cancel();
+ loadTimer3.cancel();
+ loadTimer2 = null;
+ loadTimer3 = null;
+ }
+ if (loadElement != null) {
+ DOM.setStyleAttribute(loadElement, "display", "none");
+ }
+ }
+
+ /**
+ * Checks if deferred commands are (potentially) still being executed as a
+ * result of an update from the server. Returns true if a deferred command
+ * might still be executing, false otherwise. This will not work correctly
+ * if a deferred command is added in another deferred command.
+ * <p>
+ * Used by the native "client.isActive" function.
+ * </p>
+ *
+ * @return true if deferred commands are (potentially) being executed, false
+ * otherwise
+ */
+ private boolean isExecutingDeferredCommands() {
+ Scheduler s = Scheduler.get();
+ if (s instanceof VSchedulerImpl) {
+ return ((VSchedulerImpl) s).hasWorkQueued();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Determines whether or not the loading indicator is showing.
+ *
+ * @return true if the loading indicator is visible
+ */
+ public boolean isLoadingIndicatorVisible() {
+ if (loadElement == null) {
+ return false;
+ }
+ if (loadElement.getStyle().getProperty("display").equals("none")) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static native ValueMap parseJSONResponse(String jsonText)
+ /*-{
+ try {
+ return JSON.parse(jsonText);
+ } catch (ignored) {
+ return eval('(' + jsonText + ')');
+ }
+ }-*/;
+
+ private void handleReceivedJSONMessage(Date start, String jsonText,
+ ValueMap json) {
+ handleUIDLMessage(start, jsonText, json);
+ }
+
+ protected void handleUIDLMessage(final Date start, final String jsonText,
+ final ValueMap json) {
+ VConsole.log("Handling message from server");
+ // Handle redirect
+ if (json.containsKey("redirect")) {
+ String url = json.getValueMap("redirect").getString("url");
+ VConsole.log("redirecting to " + url);
+ redirect(url);
+ return;
+ }
+
+ final MultiStepDuration handleUIDLDuration = new MultiStepDuration();
+
+ // Get security key
+ if (json.containsKey(UIDL_SECURITY_TOKEN_ID)) {
+ uidlSecurityKey = json.getString(UIDL_SECURITY_TOKEN_ID);
+ }
+ VConsole.log(" * Handling resources from server");
+
+ if (json.containsKey("resources")) {
+ ValueMap resources = json.getValueMap("resources");
+ JsArrayString keyArray = resources.getKeyArray();
+ int l = keyArray.length();
+ for (int i = 0; i < l; i++) {
+ String key = keyArray.get(i);
+ resourcesMap.put(key, resources.getAsString(key));
+ }
+ }
+ handleUIDLDuration.logDuration(
+ " * Handling resources from server completed", 10);
+
+ VConsole.log(" * Handling type inheritance map from server");
+
+ if (json.containsKey("typeInheritanceMap")) {
+ configuration.addComponentInheritanceInfo(json
+ .getValueMap("typeInheritanceMap"));
+ }
+ handleUIDLDuration.logDuration(
+ " * Handling type inheritance map from server completed", 10);
+
+ VConsole.log("Handling type mappings from server");
+
+ if (json.containsKey("typeMappings")) {
+ configuration.addComponentMappings(
+ json.getValueMap("typeMappings"), widgetSet);
+ }
+
+ VConsole.log("Handling resource dependencies");
+ if (json.containsKey("scriptDependencies")) {
+ loadScriptDependencies(json.getJSStringArray("scriptDependencies"));
+ }
+ if (json.containsKey("styleDependencies")) {
+ loadStyleDependencies(json.getJSStringArray("styleDependencies"));
+ }
+
+ handleUIDLDuration.logDuration(
+ " * Handling type mappings from server completed", 10);
+ /*
+ * Hook for e.g. TestBench to get details about server peformance
+ */
+ if (json.containsKey("timings")) {
+ serverTimingInfo = json.getValueMap("timings");
+ }
+
+ Command c = new Command() {
+ @Override
+ public void execute() {
+ handleUIDLDuration.logDuration(" * Loading widgets completed",
+ 10);
+
+ MultiStepDuration updateDuration = new MultiStepDuration();
+
+ if (json.containsKey("locales")) {
+ VConsole.log(" * Handling locales");
+ // Store locale data
+ JsArray<ValueMap> valueMapArray = json
+ .getJSValueMapArray("locales");
+ LocaleService.addLocales(valueMapArray);
+ }
+
+ updateDuration.logDuration(" * Handling locales completed", 10);
+
+ boolean repaintAll = false;
+ ValueMap meta = null;
+ if (json.containsKey("meta")) {
+ VConsole.log(" * Handling meta information");
+ meta = json.getValueMap("meta");
+ if (meta.containsKey("repaintAll")) {
+ repaintAll = true;
+ rootConnector.getWidget().clear();
+ getConnectorMap().clear();
+ if (meta.containsKey("invalidLayouts")) {
+ validatingLayouts = true;
+ zeroWidthComponents = new HashSet<ComponentConnector>();
+ zeroHeightComponents = new HashSet<ComponentConnector>();
+ }
+ }
+ if (meta.containsKey("timedRedirect")) {
+ final ValueMap timedRedirect = meta
+ .getValueMap("timedRedirect");
+ redirectTimer = new Timer() {
+ @Override
+ public void run() {
+ redirect(timedRedirect.getString("url"));
+ }
+ };
+ sessionExpirationInterval = timedRedirect
+ .getInt("interval");
+ }
+ }
+
+ updateDuration.logDuration(
+ " * Handling meta information completed", 10);
+
+ if (redirectTimer != null) {
+ redirectTimer.schedule(1000 * sessionExpirationInterval);
+ }
+
+ componentCaptionSizeChanges.clear();
+
+ int startProcessing = updateDuration.elapsedMillis();
+
+ // Ensure that all connectors that we are about to update exist
+ createConnectorsIfNeeded(json);
+
+ updateDuration.logDuration(" * Creating connectors completed",
+ 10);
+
+ // Update states, do not fire events
+ Collection<StateChangeEvent> pendingStateChangeEvents = updateConnectorState(json);
+
+ updateDuration.logDuration(
+ " * Update of connector states completed", 10);
+
+ // Update hierarchy, do not fire events
+ Collection<ConnectorHierarchyChangeEvent> pendingHierarchyChangeEvents = updateConnectorHierarchy(json);
+
+ updateDuration.logDuration(
+ " * Update of connector hierarchy completed", 10);
+
+ // Fire hierarchy change events
+ sendHierarchyChangeEvents(pendingHierarchyChangeEvents);
+
+ updateDuration.logDuration(
+ " * Hierarchy state change event processing completed",
+ 10);
+
+ // Fire state change events.
+ sendStateChangeEvents(pendingStateChangeEvents);
+
+ updateDuration.logDuration(
+ " * State change event processing completed", 10);
+
+ // Update of legacy (UIDL) style connectors
+ updateVaadin6StyleConnectors(json);
+
+ updateDuration
+ .logDuration(
+ " * Vaadin 6 style connector updates (updateFromUidl) completed",
+ 10);
+
+ // Handle any RPC invocations done on the server side
+ handleRpcInvocations(json);
+
+ updateDuration.logDuration(
+ " * Processing of RPC invocations completed", 10);
+
+ if (json.containsKey("dd")) {
+ // response contains data for drag and drop service
+ VDragAndDropManager.get().handleServerResponse(
+ json.getValueMap("dd"));
+ }
+
+ updateDuration
+ .logDuration(
+ " * Processing of drag and drop server response completed",
+ 10);
+
+ unregisterRemovedConnectors();
+
+ updateDuration.logDuration(
+ " * Unregistering of removed components completed", 10);
+
+ VConsole.log("handleUIDLMessage: "
+ + (updateDuration.elapsedMillis() - startProcessing)
+ + " ms");
+
+ LayoutManager layoutManager = getLayoutManager();
+ layoutManager.setEverythingNeedsMeasure();
+ layoutManager.layoutNow();
+
+ updateDuration
+ .logDuration(" * Layout processing completed", 10);
+
+ if (ApplicationConfiguration.isDebugMode()) {
+ VConsole.log(" * Dumping state changes to the console");
+ VConsole.dirUIDL(json, ApplicationConnection.this);
+
+ updateDuration
+ .logDuration(
+ " * Dumping state changes to the console completed",
+ 10);
+ }
+
+ if (meta != null) {
+ if (meta.containsKey("appError")) {
+ ValueMap error = meta.getValueMap("appError");
+ String html = "";
+ if (error.containsKey("caption")
+ && error.getString("caption") != null) {
+ html += "<h1>" + error.getAsString("caption")
+ + "</h1>";
+ }
+ if (error.containsKey("message")
+ && error.getString("message") != null) {
+ html += "<p>" + error.getAsString("message")
+ + "</p>";
+ }
+ String url = null;
+ if (error.containsKey("url")) {
+ url = error.getString("url");
+ }
+
+ if (html.length() != 0) {
+ /* 45 min */
+ VNotification n = VNotification
+ .createNotification(1000 * 60 * 45);
+ n.addEventListener(new NotificationRedirect(url));
+ n.show(html, VNotification.CENTERED_TOP,
+ VNotification.STYLE_SYSTEM);
+ } else {
+ redirect(url);
+ }
+ applicationRunning = false;
+ }
+ if (validatingLayouts) {
+ VConsole.printLayoutProblems(meta,
+ ApplicationConnection.this,
+ zeroHeightComponents, zeroWidthComponents);
+ zeroHeightComponents = null;
+ zeroWidthComponents = null;
+ validatingLayouts = false;
+
+ }
+ }
+
+ updateDuration.logDuration(" * Error handling completed", 10);
+
+ // TODO build profiling for widget impl loading time
+
+ lastProcessingTime = (int) ((new Date().getTime()) - start
+ .getTime());
+ totalProcessingTime += lastProcessingTime;
+
+ VConsole.log(" Processing time was "
+ + String.valueOf(lastProcessingTime) + "ms for "
+ + jsonText.length() + " characters of JSON");
+ VConsole.log("Referenced paintables: " + connectorMap.size());
+
+ endRequest();
+
+ }
+
+ /**
+ * Sends the state change events created while updating the state
+ * information.
+ *
+ * This must be called after hierarchy change listeners have been
+ * called. At least caption updates for the parent are strange if
+ * fired from state change listeners and thus calls the parent
+ * BEFORE the parent is aware of the child (through a
+ * ConnectorHierarchyChangedEvent)
+ *
+ * @param pendingStateChangeEvents
+ * The events to send
+ */
+ private void sendStateChangeEvents(
+ Collection<StateChangeEvent> pendingStateChangeEvents) {
+ VConsole.log(" * Sending state change events");
+
+ for (StateChangeEvent sce : pendingStateChangeEvents) {
+ try {
+ sce.getConnector().fireEvent(sce);
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+
+ }
+
+ private void unregisterRemovedConnectors() {
+ int unregistered = 0;
+ List<ServerConnector> currentConnectors = new ArrayList<ServerConnector>(
+ connectorMap.getConnectors());
+ for (ServerConnector c : currentConnectors) {
+ if (c.getParent() != null) {
+ if (!c.getParent().getChildren().contains(c)) {
+ VConsole.error("ERROR: Connector is connected to a parent but the parent does not contain the connector");
+ }
+ } else if ((c instanceof RootConnector && c == getRootConnector())) {
+ // RootConnector for this connection, leave as-is
+ } else if (c instanceof WindowConnector
+ && getRootConnector().hasSubWindow(
+ (WindowConnector) c)) {
+ // Sub window attached to this RootConnector, leave
+ // as-is
+ } else {
+ // The connector has been detached from the
+ // hierarchy, unregister it and any possible
+ // children. The RootConnector should never be
+ // unregistered even though it has no parent.
+ connectorMap.unregisterConnector(c);
+ unregistered++;
+ }
+
+ }
+
+ VConsole.log("* Unregistered " + unregistered + " connectors");
+ }
+
+ private void createConnectorsIfNeeded(ValueMap json) {
+ VConsole.log(" * Creating connectors (if needed)");
+
+ if (!json.containsKey("types")) {
+ return;
+ }
+
+ ValueMap types = json.getValueMap("types");
+ JsArrayString keyArray = types.getKeyArray();
+ for (int i = 0; i < keyArray.length(); i++) {
+ try {
+ String connectorId = keyArray.get(i);
+ int connectorType = Integer.parseInt(types
+ .getString((connectorId)));
+ ServerConnector connector = connectorMap
+ .getConnector(connectorId);
+ if (connector != null) {
+ continue;
+ }
+
+ Class<? extends ServerConnector> connectorClass = configuration
+ .getConnectorClassByEncodedTag(connectorType);
+
+ // Connector does not exist so we must create it
+ if (connectorClass != RootConnector.class) {
+ // create, initialize and register the paintable
+ getConnector(connectorId, connectorType);
+ } else {
+ // First RootConnector update. Before this the
+ // RootConnector has been created but not
+ // initialized as the connector id has not been
+ // known
+ connectorMap.registerConnector(connectorId,
+ rootConnector);
+ rootConnector.doInit(connectorId,
+ ApplicationConnection.this);
+ }
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+ }
+
+ private void updateVaadin6StyleConnectors(ValueMap json) {
+ JsArray<ValueMap> changes = json.getJSValueMapArray("changes");
+ int length = changes.length();
+
+ VConsole.log(" * Passing UIDL to Vaadin 6 style connectors");
+ // update paintables
+ for (int i = 0; i < length; i++) {
+ try {
+ final UIDL change = changes.get(i).cast();
+ final UIDL uidl = change.getChildUIDL(0);
+ String connectorId = uidl.getId();
+
+ final ComponentConnector legacyConnector = (ComponentConnector) connectorMap
+ .getConnector(connectorId);
+ if (legacyConnector instanceof Paintable) {
+ ((Paintable) legacyConnector).updateFromUIDL(uidl,
+ ApplicationConnection.this);
+ } else if (legacyConnector == null) {
+ VConsole.error("Received update for "
+ + uidl.getTag()
+ + ", but there is no such paintable ("
+ + connectorId + ") rendered.");
+ } else {
+ VConsole.error("Server sent Vaadin 6 style updates for "
+ + Util.getConnectorString(legacyConnector)
+ + " but this is not a Vaadin 6 Paintable");
+ }
+
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+ }
+
+ private void sendHierarchyChangeEvents(
+ Collection<ConnectorHierarchyChangeEvent> pendingHierarchyChangeEvents) {
+ if (pendingHierarchyChangeEvents.isEmpty()) {
+ return;
+ }
+
+ VConsole.log(" * Sending hierarchy change events");
+ for (ConnectorHierarchyChangeEvent event : pendingHierarchyChangeEvents) {
+ try {
+ event.getConnector().fireEvent(event);
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+
+ }
+
+ private Collection<StateChangeEvent> updateConnectorState(
+ ValueMap json) {
+ ArrayList<StateChangeEvent> events = new ArrayList<StateChangeEvent>();
+ VConsole.log(" * Updating connector states");
+ if (!json.containsKey("state")) {
+ return events;
+ }
+ // set states for all paintables mentioned in "state"
+ ValueMap states = json.getValueMap("state");
+ JsArrayString keyArray = states.getKeyArray();
+ for (int i = 0; i < keyArray.length(); i++) {
+ try {
+ String connectorId = keyArray.get(i);
+ ServerConnector connector = connectorMap
+ .getConnector(connectorId);
+ if (null != connector) {
+
+ JSONObject stateJson = new JSONObject(
+ states.getJavaScriptObject(connectorId));
+
+ if (connector instanceof HasJavaScriptConnectorHelper) {
+ ((HasJavaScriptConnectorHelper) connector)
+ .getJavascriptConnectorHelper()
+ .setNativeState(
+ stateJson.getJavaScriptObject());
+ }
+
+ SharedState state = connector.getState();
+ JsonDecoder.decodeValue(new Type(state.getClass()
+ .getName(), null), stateJson, state,
+ ApplicationConnection.this);
+
+ StateChangeEvent event = GWT
+ .create(StateChangeEvent.class);
+ event.setConnector(connector);
+ events.add(event);
+ }
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+
+ return events;
+ }
+
+ /**
+ * Updates the connector hierarchy and returns a list of events that
+ * should be fired after update of the hierarchy and the state is
+ * done.
+ *
+ * @param json
+ * The JSON containing the hierarchy information
+ * @return A collection of events that should be fired when update
+ * of hierarchy and state is complete
+ */
+ private Collection<ConnectorHierarchyChangeEvent> updateConnectorHierarchy(
+ ValueMap json) {
+ List<ConnectorHierarchyChangeEvent> events = new LinkedList<ConnectorHierarchyChangeEvent>();
+
+ VConsole.log(" * Updating connector hierarchy");
+ if (!json.containsKey("hierarchy")) {
+ return events;
+ }
+
+ ValueMap hierarchies = json.getValueMap("hierarchy");
+ JsArrayString hierarchyKeys = hierarchies.getKeyArray();
+ for (int i = 0; i < hierarchyKeys.length(); i++) {
+ try {
+ String connectorId = hierarchyKeys.get(i);
+ ServerConnector parentConnector = connectorMap
+ .getConnector(connectorId);
+ JsArrayString childConnectorIds = hierarchies
+ .getJSStringArray(connectorId);
+ int childConnectorSize = childConnectorIds.length();
+
+ List<ServerConnector> newChildren = new ArrayList<ServerConnector>();
+ List<ComponentConnector> newComponents = new ArrayList<ComponentConnector>();
+ for (int connectorIndex = 0; connectorIndex < childConnectorSize; connectorIndex++) {
+ String childConnectorId = childConnectorIds
+ .get(connectorIndex);
+ ServerConnector childConnector = connectorMap
+ .getConnector(childConnectorId);
+ if (childConnector == null) {
+ VConsole.error("Hierarchy claims that "
+ + childConnectorId + " is a child for "
+ + connectorId + " ("
+ + parentConnector.getClass().getName()
+ + ") but no connector with id "
+ + childConnectorId
+ + " has been registered");
+ continue;
+ }
+ newChildren.add(childConnector);
+ if (childConnector instanceof ComponentConnector) {
+ newComponents
+ .add((ComponentConnector) childConnector);
+ } else if (!(childConnector instanceof AbstractExtensionConnector)) {
+ throw new IllegalStateException(
+ Util.getConnectorString(childConnector)
+ + " is not a ComponentConnector nor an AbstractExtensionConnector");
+ }
+ if (childConnector.getParent() != parentConnector) {
+ // Avoid extra calls to setParent
+ childConnector.setParent(parentConnector);
+ }
+ }
+
+ // TODO This check should be done on the server side in
+ // the future so the hierarchy update is only sent when
+ // something actually has changed
+ List<ServerConnector> oldChildren = parentConnector
+ .getChildren();
+ boolean actuallyChanged = !Util.collectionsEquals(
+ oldChildren, newChildren);
+
+ if (!actuallyChanged) {
+ continue;
+ }
+
+ if (parentConnector instanceof ComponentContainerConnector) {
+ ComponentContainerConnector ccc = (ComponentContainerConnector) parentConnector;
+ List<ComponentConnector> oldComponents = ccc
+ .getChildComponents();
+ if (!Util.collectionsEquals(oldComponents,
+ newComponents)) {
+ // Fire change event if the hierarchy has
+ // changed
+ ConnectorHierarchyChangeEvent event = GWT
+ .create(ConnectorHierarchyChangeEvent.class);
+ event.setOldChildren(oldComponents);
+ event.setConnector(parentConnector);
+ ccc.setChildComponents(newComponents);
+ events.add(event);
+ }
+ } else if (!newComponents.isEmpty()) {
+ VConsole.error("Hierachy claims "
+ + Util.getConnectorString(parentConnector)
+ + " has component children even though it isn't a ComponentContainerConnector");
+ }
+
+ parentConnector.setChildren(newChildren);
+
+ // Remove parent for children that are no longer
+ // attached to this (avoid updating children if they
+ // have already been assigned to a new parent)
+ for (ServerConnector oldChild : oldChildren) {
+ if (oldChild.getParent() != parentConnector) {
+ continue;
+ }
+
+ // TODO This could probably be optimized
+ if (!newChildren.contains(oldChild)) {
+ oldChild.setParent(null);
+ }
+ }
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+ return events;
+
+ }
+
+ private void handleRpcInvocations(ValueMap json) {
+ if (json.containsKey("rpc")) {
+ VConsole.log(" * Performing server to client RPC calls");
+
+ JSONArray rpcCalls = new JSONArray(
+ json.getJavaScriptObject("rpc"));
+
+ int rpcLength = rpcCalls.size();
+ for (int i = 0; i < rpcLength; i++) {
+ try {
+ JSONArray rpcCall = (JSONArray) rpcCalls.get(i);
+ rpcManager.parseAndApplyInvocation(rpcCall,
+ ApplicationConnection.this);
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ }
+ }
+
+ }
+
+ };
+ ApplicationConfiguration.runWhenDependenciesLoaded(c);
+ }
+
+ private void loadStyleDependencies(JsArrayString dependencies) {
+ // Assuming no reason to interpret in a defined order
+ ResourceLoadListener resourceLoadListener = new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ ApplicationConfiguration.endDependencyLoading();
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ VConsole.error(event.getResourceUrl()
+ + " could not be loaded, or the load detection failed because the stylesheet is empty.");
+ // The show must go on
+ onLoad(event);
+ }
+ };
+ ResourceLoader loader = ResourceLoader.get();
+ for (int i = 0; i < dependencies.length(); i++) {
+ String url = translateVaadinUri(dependencies.get(i));
+ ApplicationConfiguration.startDependencyLoading();
+ loader.loadStylesheet(url, resourceLoadListener);
+ }
+ }
+
+ private void loadScriptDependencies(final JsArrayString dependencies) {
+ if (dependencies.length() == 0) {
+ return;
+ }
+
+ // Listener that loads the next when one is completed
+ ResourceLoadListener resourceLoadListener = new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ if (dependencies.length() != 0) {
+ String url = translateVaadinUri(dependencies.shift());
+ ApplicationConfiguration.startDependencyLoading();
+ // Load next in chain (hopefully already preloaded)
+ event.getResourceLoader().loadScript(url, this);
+ }
+ // Call start for next before calling end for current
+ ApplicationConfiguration.endDependencyLoading();
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ VConsole.error(event.getResourceUrl() + " could not be loaded.");
+ // The show must go on
+ onLoad(event);
+ }
+ };
+
+ ResourceLoader loader = ResourceLoader.get();
+
+ // Start chain by loading first
+ String url = translateVaadinUri(dependencies.shift());
+ ApplicationConfiguration.startDependencyLoading();
+ loader.loadScript(url, resourceLoadListener);
+
+ // Preload all remaining
+ for (int i = 0; i < dependencies.length(); i++) {
+ String preloadUrl = translateVaadinUri(dependencies.get(i));
+ loader.preloadResource(preloadUrl, null);
+ }
+ }
+
+ // Redirect browser, null reloads current page
+ private static native void redirect(String url)
+ /*-{
+ if (url) {
+ $wnd.location = url;
+ } else {
+ $wnd.location.reload(false);
+ }
+ }-*/;
+
+ private void addVariableToQueue(String connectorId, String variableName,
+ Object value, boolean immediate) {
+ // note that type is now deduced from value
+ // TODO could eliminate invocations of same shared variable setter
+ addMethodInvocationToQueue(new MethodInvocation(connectorId,
+ UPDATE_VARIABLE_INTERFACE, UPDATE_VARIABLE_METHOD,
+ new Object[] { variableName, new UidlValue(value) }), immediate);
+ }
+
+ /**
+ * Adds an explicit RPC method invocation to the send queue.
+ *
+ * @since 7.0
+ *
+ * @param invocation
+ * RPC method invocation
+ * @param immediate
+ * true to trigger sending within a short time window (possibly
+ * combining subsequent calls to a single request), false to let
+ * the framework delay sending of RPC calls and variable changes
+ * until the next immediate change
+ */
+ public void addMethodInvocationToQueue(MethodInvocation invocation,
+ boolean immediate) {
+ pendingInvocations.add(invocation);
+ if (immediate) {
+ sendPendingVariableChanges();
+ }
+ }
+
+ /**
+ * This method sends currently queued variable changes to server. It is
+ * called when immediate variable update must happen.
+ *
+ * To ensure correct order for variable changes (due servers multithreading
+ * or network), we always wait for active request to be handler before
+ * sending a new one. If there is an active request, we will put varible
+ * "burst" to queue that will be purged after current request is handled.
+ *
+ */
+ public void sendPendingVariableChanges() {
+ if (!deferedSendPending) {
+ deferedSendPending = true;
+ Scheduler.get().scheduleDeferred(sendPendingCommand);
+ }
+ }
+
+ private final ScheduledCommand sendPendingCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ deferedSendPending = false;
+ doSendPendingVariableChanges();
+ }
+ };
+ private boolean deferedSendPending = false;
+
+ @SuppressWarnings("unchecked")
+ private void doSendPendingVariableChanges() {
+ if (applicationRunning) {
+ if (hasActiveRequest()) {
+ // skip empty queues if there are pending bursts to be sent
+ if (pendingInvocations.size() > 0 || pendingBursts.size() == 0) {
+ pendingBursts.add(pendingInvocations);
+ pendingInvocations = new ArrayList<MethodInvocation>();
+ }
+ } else {
+ buildAndSendVariableBurst(pendingInvocations, 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 never be
+ * updated after this.
+ *
+ * @param pendingInvocations
+ * List of RPC method invocations to send
+ * @param forceSync
+ * Should we use synchronous request?
+ */
+ private void buildAndSendVariableBurst(
+ ArrayList<MethodInvocation> pendingInvocations, boolean forceSync) {
+ final StringBuffer req = new StringBuffer();
+
+ while (!pendingInvocations.isEmpty()) {
+ if (ApplicationConfiguration.isDebugMode()) {
+ Util.logVariableBurst(this, pendingInvocations);
+ }
+
+ JSONArray reqJson = new JSONArray();
+
+ for (MethodInvocation invocation : pendingInvocations) {
+ JSONArray invocationJson = new JSONArray();
+ invocationJson.set(0,
+ new JSONString(invocation.getConnectorId()));
+ invocationJson.set(1,
+ new JSONString(invocation.getInterfaceName()));
+ invocationJson.set(2,
+ new JSONString(invocation.getMethodName()));
+ JSONArray paramJson = new JSONArray();
+ boolean restrictToInternalTypes = isLegacyVariableChange(invocation);
+ for (int i = 0; i < invocation.getParameters().length; ++i) {
+ // TODO non-static encoder? type registration?
+ paramJson.set(i, JsonEncoder.encode(
+ invocation.getParameters()[i],
+ restrictToInternalTypes, this));
+ }
+ invocationJson.set(3, paramJson);
+ reqJson.set(reqJson.size(), invocationJson);
+ }
+
+ // escape burst separators (if any)
+ req.append(escapeBurstContents(reqJson.toString()));
+
+ pendingInvocations.clear();
+ // Append all the bursts to this synchronous request
+ if (forceSync && !pendingBursts.isEmpty()) {
+ pendingInvocations = pendingBursts.get(0);
+ pendingBursts.remove(0);
+ req.append(VAR_BURST_SEPARATOR);
+ }
+ }
+
+ // Include the browser detail parameters if they aren't already sent
+ String extraParams;
+ if (!getConfiguration().isBrowserDetailsSent()) {
+ extraParams = getNativeBrowserDetailsParameters(getConfiguration()
+ .getRootPanelId());
+ getConfiguration().setBrowserDetailsSent();
+ } else {
+ extraParams = "";
+ }
+ if (!getConfiguration().isWidgetsetVersionSent()) {
+ if (!extraParams.isEmpty()) {
+ extraParams += "&";
+ }
+ String widgetsetVersion = ApplicationConfiguration.VERSION;
+ extraParams += "wsver=" + widgetsetVersion;
+
+ getConfiguration().setWidgetsetVersionSent();
+ }
+ makeUidlRequest(req.toString(), extraParams, forceSync);
+ }
+
+ private boolean isLegacyVariableChange(MethodInvocation invocation) {
+ return ApplicationConnection.UPDATE_VARIABLE_METHOD.equals(invocation
+ .getInterfaceName())
+ && ApplicationConnection.UPDATE_VARIABLE_METHOD
+ .equals(invocation.getMethodName());
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ ServerConnector newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ String newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ int newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ long newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ float newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ double newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param newValue
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+
+ public void updateVariable(String paintableId, String variableName,
+ boolean newValue, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, newValue, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ * <p>
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ * </p>
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param map
+ * the new values to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ Map<String, Object> map, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, map, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ *
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update.
+ *
+ * A null array is sent as an empty array.
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param values
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ String[] values, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, values, immediate);
+ }
+
+ /**
+ * Sends a new value for the given paintables given variable to the server.
+ *
+ * The update is actually queued to be sent at a suitable time. If immediate
+ * is true, the update is sent as soon as possible. If immediate is false,
+ * the update will be sent along with the next immediate update. </p>
+ *
+ * A null array is sent as an empty array.
+ *
+ *
+ * @param paintableId
+ * the id of the paintable that owns the variable
+ * @param variableName
+ * the name of the variable
+ * @param values
+ * the new value to be sent
+ * @param immediate
+ * true if the update is to be sent as soon as possible
+ */
+ public void updateVariable(String paintableId, String variableName,
+ Object[] values, boolean immediate) {
+ addVariableToQueue(paintableId, variableName, values, immediate);
+ }
+
+ /**
+ * Encode burst separator characters in a String for transport over the
+ * network. This protects from separator injection attacks.
+ *
+ * @param value
+ * to encode
+ * @return encoded value
+ */
+ protected String escapeBurstContents(String value) {
+ final StringBuilder result = new StringBuilder();
+ for (int i = 0; i < value.length(); ++i) {
+ char character = value.charAt(i);
+ switch (character) {
+ case VAR_ESCAPE_CHARACTER:
+ // fall-through - escape character is duplicated
+ case VAR_BURST_SEPARATOR:
+ result.append(VAR_ESCAPE_CHARACTER);
+ // encode as letters for easier reading
+ result.append(((char) (character + 0x30)));
+ break;
+ default:
+ // the char is not a special one - add it to the result as is
+ result.append(character);
+ break;
+ }
+ }
+ return result.toString();
+ }
+
+ private boolean runningLayout = false;
+
+ /**
+ * Causes a re-calculation/re-layout of all paintables in a container.
+ *
+ * @param container
+ */
+ public void runDescendentsLayout(HasWidgets container) {
+ if (runningLayout) {
+ 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() {
+ Duration duration = new Duration();
+
+ layoutManager.forceLayout();
+
+ VConsole.log("forceLayout in " + duration.elapsedMillis() + " ms");
+ }
+
+ 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 (getConnectorMap().isConnector(child)) {
+
+ 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(ComponentConnector paintable) {
+ return false;
+ }
+
+ /**
+ * Converts relative sizes into pixel sizes.
+ *
+ * @param child
+ * @return true if the child has a relative size
+ */
+ public boolean handleComponentRelativeSize(Widget widget) {
+ return handleComponentRelativeSize(connectorMap.getConnector(widget));
+
+ }
+
+ @Deprecated
+ public ComponentConnector getPaintable(UIDL uidl) {
+ // Non-component connectors shouldn't be painted from legacy connectors
+ return (ComponentConnector) getConnector(uidl.getId(),
+ Integer.parseInt(uidl.getTag()));
+ }
+
+ /**
+ * Get either an existing ComponentConnector or create a new
+ * ComponentConnector with the given type and id.
+ *
+ * If a ComponentConnector with the given id already exists, returns it.
+ * Otherwise creates and registers a new ComponentConnector of the given
+ * type.
+ *
+ * @param connectorId
+ * Id of the paintable
+ * @param connectorType
+ * Type of the connector, as passed from the server side
+ *
+ * @return Either an existing ComponentConnector or a new ComponentConnector
+ * of the given type
+ */
+ public ServerConnector getConnector(String connectorId, int connectorType) {
+ if (!connectorMap.hasConnector(connectorId)) {
+ return createAndRegisterConnector(connectorId, connectorType);
+ }
+ return connectorMap.getConnector(connectorId);
+ }
+
+ /**
+ * Creates a new ServerConnector with the given type and id.
+ *
+ * Creates and registers a new ServerConnector of the given type. Should
+ * never be called with the connector id of an existing connector.
+ *
+ * @param connectorId
+ * Id of the new connector
+ * @param connectorType
+ * Type of the connector, as passed from the server side
+ *
+ * @return A new ServerConnector of the given type
+ */
+ private ServerConnector createAndRegisterConnector(String connectorId,
+ int connectorType) {
+ // Create and register a new connector with the given type
+ ServerConnector p = widgetSet.createConnector(connectorType,
+ configuration);
+ connectorMap.registerConnector(connectorId, p);
+ p.doInit(connectorId, this);
+
+ return p;
+ }
+
+ /**
+ * Gets a recource that has been pre-loaded via UIDL, such as custom
+ * layouts.
+ *
+ * @param name
+ * identifier of the resource to get
+ * @return the resource
+ */
+ public String getResource(String name) {
+ return resourcesMap.get(name);
+ }
+
+ /**
+ * Singleton method to get instance of app's context menu.
+ *
+ * @return VContextMenu object
+ */
+ public VContextMenu getContextMenu() {
+ if (contextMenu == null) {
+ contextMenu = new VContextMenu();
+ DOM.setElementProperty(contextMenu.getElement(), "id",
+ "PID_VAADIN_CM");
+ }
+ return contextMenu;
+ }
+
+ /**
+ * Translates custom protocols in UIDL 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 uidlUri
+ * Vaadin URI from uidl
+ * @return translated URI ready for browser
+ */
+ public String translateVaadinUri(String uidlUri) {
+ if (uidlUri == null) {
+ return null;
+ }
+ if (uidlUri.startsWith("theme://")) {
+ final String themeUri = configuration.getThemeUri();
+ if (themeUri == null) {
+ VConsole.error("Theme not set: ThemeResource will not be found. ("
+ + uidlUri + ")");
+ }
+ uidlUri = themeUri + uidlUri.substring(7);
+ }
+
+ if (uidlUri.startsWith(CONNECTOR_PROTOCOL_PREFIX)) {
+ // getAppUri *should* always end with /
+ // substring *should* always start with / (connector:///foo.bar
+ // without connector://)
+ uidlUri = APP_PROTOCOL_PREFIX + CONNECTOR_RESOURCE_PREFIX
+ + uidlUri.substring(CONNECTOR_PROTOCOL_PREFIX.length());
+ // Let translation of app:// urls take care of the rest
+ }
+ if (uidlUri.startsWith(APP_PROTOCOL_PREFIX)) {
+ String relativeUrl = uidlUri
+ .substring(APP_PROTOCOL_PREFIX.length());
+ if (getConfiguration().usePortletURLs()) {
+ // Should put path in v-resourcePath parameter and append query
+ // params to base portlet url
+ String[] parts = relativeUrl.split("\\?", 2);
+ String path = parts[0];
+
+ String url = getConfiguration().getPortletResourceUrl();
+
+ // If there's a "?" followed by something, append it as a query
+ // string to the base URL
+ if (parts.length > 1) {
+ String appUrlParams = parts[1];
+ url = addGetParameters(url, appUrlParams);
+ }
+ if (!path.startsWith("/")) {
+ path = '/' + path;
+ }
+ String pathParam = V_RESOURCE_PATH + "="
+ + URL.encodeQueryString(path);
+ url = addGetParameters(url, pathParam);
+ uidlUri = url;
+ } else {
+ uidlUri = getAppUri() + relativeUrl;
+ }
+ }
+ return uidlUri;
+ }
+
+ /**
+ * Gets the URI for the current theme. Can be used to reference theme
+ * resources.
+ *
+ * @return URI to the current theme
+ */
+ 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 VNotification.EventListener {
+ String url;
+
+ NotificationRedirect(String url) {
+ this.url = url;
+ }
+
+ @Override
+ public void notificationHidden(HideEvent event) {
+ redirect(url);
+ }
+
+ }
+
+ /* Extended title handling */
+
+ private final VTooltip tooltip = new VTooltip(this);
+
+ private ConnectorMap connectorMap = GWT.create(ConnectorMap.class);
+
+ protected String getUidlSecurityKey() {
+ return uidlSecurityKey;
+ }
+
+ /**
+ * Use to notify that the given component's caption has changed; layouts may
+ * have to be recalculated.
+ *
+ * @param component
+ * the Paintable whose caption has changed
+ */
+ public void captionSizeUpdated(Widget widget) {
+ componentCaptionSizeChanges.add(widget);
+ }
+
+ /**
+ * Gets the main view
+ *
+ * @return the main view
+ */
+ public RootConnector getRootConnector() {
+ return rootConnector;
+ }
+
+ /**
+ * Gets the {@link ApplicationConfiguration} for the current application.
+ *
+ * @see ApplicationConfiguration
+ * @return the configuration for this application
+ */
+ public ApplicationConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * Checks if there is a registered server side listener for the event. The
+ * list of events which has server side listeners is updated automatically
+ * before the component is updated so the value is correct if called from
+ * updatedFromUIDL.
+ *
+ * @param paintable
+ * The connector to register event listeners for
+ * @param eventIdentifier
+ * The identifier for the event
+ * @return true if at least one listener has been registered on server side
+ * for the event identified by eventIdentifier.
+ * @deprecated Use {@link ComponentState#hasEventListener(String)} instead
+ */
+ @Deprecated
+ public boolean hasEventListeners(ComponentConnector paintable,
+ String eventIdentifier) {
+ return paintable.hasEventListener(eventIdentifier);
+ }
+
+ /**
+ * Adds the get parameters to the uri and returns the new uri that contains
+ * the parameters.
+ *
+ * @param uri
+ * The uri to which the parameters should be added.
+ * @param extraParams
+ * One or more parameters in the format "a=b" or "c=d&e=f". An
+ * empty string is allowed but will not modify the url.
+ * @return The modified URI with the get parameters in extraParams added.
+ */
+ public static String addGetParameters(String uri, String extraParams) {
+ if (extraParams == null || extraParams.length() == 0) {
+ return uri;
+ }
+ // RFC 3986: The query component is indicated by the first question
+ // mark ("?") character and terminated by a number sign ("#") character
+ // or by the end of the URI.
+ String fragment = null;
+ int hashPosition = uri.indexOf('#');
+ if (hashPosition != -1) {
+ // Fragment including "#"
+ fragment = uri.substring(hashPosition);
+ // The full uri before the fragment
+ uri = uri.substring(0, hashPosition);
+ }
+
+ if (uri.contains("?")) {
+ uri += "&";
+ } else {
+ uri += "?";
+ }
+ uri += extraParams;
+
+ if (fragment != null) {
+ uri += fragment;
+ }
+
+ return uri;
+ }
+
+ ConnectorMap getConnectorMap() {
+ return connectorMap;
+ }
+
+ @Deprecated
+ public void unregisterPaintable(ServerConnector p) {
+ System.out.println("unregisterPaintable (unnecessarily) called for "
+ + Util.getConnectorString(p));
+ // connectorMap.unregisterConnector(p);
+ }
+
+ /**
+ * Get VTooltip instance related to application connection
+ *
+ * @return VTooltip instance
+ */
+ public VTooltip getVTooltip() {
+ return tooltip;
+ }
+
+ /**
+ * Method provided for backwards compatibility. Duties previously done by
+ * this method is now handled by the state change event handler in
+ * AbstractComponentConnector. The only function this method has is to
+ * return true if the UIDL is a "cached" update.
+ *
+ * @param component
+ * @param uidl
+ * @param manageCaption
+ * @return
+ */
+ @Deprecated
+ public boolean updateComponent(Widget component, UIDL uidl,
+ boolean manageCaption) {
+ ComponentConnector connector = getConnectorMap()
+ .getConnector(component);
+ if (!AbstractComponentConnector.isRealUpdate(uidl)) {
+ return true;
+ }
+
+ if (!manageCaption) {
+ VConsole.error(Util.getConnectorString(connector)
+ + " called updateComponent with manageCaption=false. The parameter was ignored - override delegateCaption() to return false instead. It is however not recommended to use caption this way at all.");
+ }
+ return false;
+ }
+
+ @Deprecated
+ public boolean hasEventListeners(Widget widget, String eventIdentifier) {
+ return hasEventListeners(getConnectorMap().getConnector(widget),
+ eventIdentifier);
+ }
+
+ LayoutManager getLayoutManager() {
+ return layoutManager;
+ }
+
+ public SerializerMap getSerializerMap() {
+ return serializerMap;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java b/client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java
new file mode 100644
index 0000000000..32bb0b8eed
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/BrowserInfo.java
@@ -0,0 +1,378 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.RootPanel;
+import com.vaadin.shared.VBrowserDetails;
+
+/**
+ * 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 final String BROWSER_OPERA = "op";
+ private static final String BROWSER_IE = "ie";
+ private static final String BROWSER_FIREFOX = "ff";
+ private static final String BROWSER_SAFARI = "sa";
+
+ public static final String ENGINE_GECKO = "gecko";
+ public static final String ENGINE_WEBKIT = "webkit";
+ public static final String ENGINE_PRESTO = "presto";
+ public static final String ENGINE_TRIDENT = "trident";
+
+ private static final String OS_WINDOWS = "win";
+ private static final String OS_LINUX = "lin";
+ private static final String OS_MACOSX = "mac";
+ private static final String OS_ANDROID = "android";
+ private static final String OS_IOS = "ios";
+
+ // Common CSS class for all touch devices
+ private static final String UI_TOUCH = "touch";
+
+ private static BrowserInfo instance;
+
+ private static String cssClass = null;
+
+ static {
+ // Add browser dependent v-* 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 VBrowserDetails browserDetails;
+ private boolean touchDevice;
+
+ private BrowserInfo() {
+ browserDetails = new VBrowserDetails(getBrowserString());
+ if (browserDetails.isIE()) {
+ // Use document mode instead user agent to accurately detect how we
+ // are rendering
+ int documentMode = getIEDocumentMode();
+ if (documentMode != -1) {
+ browserDetails.setIEMode(documentMode);
+ }
+ }
+
+ if (browserDetails.isChrome()) {
+ touchDevice = detectChromeTouchDevice();
+ } else {
+ touchDevice = detectTouchDevice();
+ }
+ }
+
+ private native boolean detectTouchDevice()
+ /*-{
+ try { document.createEvent("TouchEvent");return true;} catch(e){return false;};
+ }-*/;
+
+ private native boolean detectChromeTouchDevice()
+ /*-{
+ return ("ontouchstart" in window);
+ }-*/;
+
+ private native int getIEDocumentMode()
+ /*-{
+ var mode = $wnd.document.documentMode;
+ if (!mode)
+ return -1;
+ return mode;
+ }-*/;
+
+ /**
+ * Returns a string representing the browser in use, for use in CSS
+ * classnames. The classnames will be space separated abbreviations,
+ * optionally with a version appended.
+ *
+ * Abbreviations: 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: ".v-ie .v-ie6 .v-ie60", Firefox 3.0.4:
+ * ".v-ff .v-ff3 .v-ff30", Opera 9.60: ".v-op .v-op9 .v-op960", Opera 10.10:
+ * ".v-op .v-op10 .v-op1010"
+ *
+ * @return
+ */
+ public String getCSSClass() {
+ String prefix = "v-";
+
+ if (cssClass == null) {
+ String browserIdentifier = "";
+ String majorVersionClass = "";
+ String minorVersionClass = "";
+ String browserEngineClass = "";
+
+ if (browserDetails.isFirefox()) {
+ browserIdentifier = BROWSER_FIREFOX;
+ majorVersionClass = browserIdentifier
+ + browserDetails.getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_GECKO;
+ } else if (browserDetails.isChrome()) {
+ // TODO update when Chrome is more stable
+ browserIdentifier = BROWSER_SAFARI;
+ majorVersionClass = "ch";
+ browserEngineClass = ENGINE_WEBKIT;
+ } else if (browserDetails.isSafari()) {
+ browserIdentifier = BROWSER_SAFARI;
+ majorVersionClass = browserIdentifier
+ + browserDetails.getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_WEBKIT;
+ } else if (browserDetails.isIE()) {
+ browserIdentifier = BROWSER_IE;
+ majorVersionClass = browserIdentifier
+ + browserDetails.getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_TRIDENT;
+ } else if (browserDetails.isOpera()) {
+ browserIdentifier = BROWSER_OPERA;
+ majorVersionClass = browserIdentifier
+ + browserDetails.getBrowserMajorVersion();
+ minorVersionClass = majorVersionClass
+ + browserDetails.getBrowserMinorVersion();
+ browserEngineClass = ENGINE_PRESTO;
+ }
+
+ cssClass = prefix + browserIdentifier;
+ if (!"".equals(majorVersionClass)) {
+ cssClass = cssClass + " " + prefix + majorVersionClass;
+ }
+ if (!"".equals(minorVersionClass)) {
+ cssClass = cssClass + " " + prefix + minorVersionClass;
+ }
+ if (!"".equals(browserEngineClass)) {
+ cssClass = cssClass + " " + prefix + browserEngineClass;
+ }
+ String osClass = getOperatingSystemClass();
+ if (osClass != null) {
+ cssClass = cssClass + " " + prefix + osClass;
+ }
+ if (isTouchDevice()) {
+ cssClass = cssClass + " " + prefix + UI_TOUCH;
+ }
+ }
+
+ return cssClass;
+ }
+
+ private String getOperatingSystemClass() {
+ if (browserDetails.isAndroid()) {
+ return OS_ANDROID;
+ } else if (browserDetails.isIOS()) {
+ return OS_IOS;
+ } else if (browserDetails.isWindows()) {
+ return OS_WINDOWS;
+ } else if (browserDetails.isLinux()) {
+ return OS_LINUX;
+ } else if (browserDetails.isMacOSX()) {
+ return OS_MACOSX;
+ }
+ // Unknown OS
+ return null;
+ }
+
+ public boolean isIE() {
+ return browserDetails.isIE();
+ }
+
+ public boolean isFirefox() {
+ return browserDetails.isFirefox();
+ }
+
+ public boolean isSafari() {
+ return browserDetails.isSafari();
+ }
+
+ public boolean isIE8() {
+ return isIE() && browserDetails.getBrowserMajorVersion() == 8;
+ }
+
+ public boolean isIE9() {
+ return isIE() && browserDetails.getBrowserMajorVersion() == 9;
+ }
+
+ public boolean isChrome() {
+ return browserDetails.isChrome();
+ }
+
+ public boolean isGecko() {
+ return browserDetails.isGecko();
+ }
+
+ public boolean isWebkit() {
+ return browserDetails.isWebKit();
+ }
+
+ /**
+ * Returns the Gecko version if the browser is Gecko based. The Gecko
+ * version for Firefox 2 is 1.8 and 1.9 for Firefox 3.
+ *
+ * @return The Gecko version or -1 if the browser is not Gecko based
+ */
+ public float getGeckoVersion() {
+ if (!browserDetails.isGecko()) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserEngineVersion();
+ }
+
+ /**
+ * Returns the WebKit version if the browser is WebKit based. The WebKit
+ * version returned is the major version e.g., 523.
+ *
+ * @return The WebKit version or -1 if the browser is not WebKit based
+ */
+ public float getWebkitVersion() {
+ if (!browserDetails.isWebKit()) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserEngineVersion();
+ }
+
+ public float getIEVersion() {
+ if (!browserDetails.isIE()) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserMajorVersion();
+ }
+
+ public float getOperaVersion() {
+ if (!browserDetails.isOpera()) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserMajorVersion();
+ }
+
+ public boolean isOpera() {
+ return browserDetails.isOpera();
+ }
+
+ public boolean isOpera10() {
+ return browserDetails.isOpera()
+ && browserDetails.getBrowserMajorVersion() == 10;
+ }
+
+ public boolean isOpera11() {
+ return browserDetails.isOpera()
+ && browserDetails.getBrowserMajorVersion() == 11;
+ }
+
+ public native static String getBrowserString()
+ /*-{
+ return $wnd.navigator.userAgent;
+ }-*/;
+
+ public native int getScreenWidth()
+ /*-{
+ return $wnd.screen.width;
+ }-*/;
+
+ public native int getScreenHeight()
+ /*-{
+ return $wnd.screen.height;
+ }-*/;
+
+ /**
+ * @return true if the browser runs on a touch based device.
+ */
+ public boolean isTouchDevice() {
+ return touchDevice;
+ }
+
+ /**
+ * Indicates whether the browser might require juggling to properly update
+ * sizes inside elements with overflow: auto.
+ *
+ * @return <code>true</code> if the browser requires the workaround,
+ * otherwise <code>false</code>
+ */
+ public boolean requiresOverflowAutoFix() {
+ return (getWebkitVersion() > 0 || getOperaVersion() >= 11)
+ && Util.getNativeScrollbarSize() > 0;
+ }
+
+ /**
+ * Checks if the browser is run on iOS
+ *
+ * @return true if the browser is run on iOS, false otherwise
+ */
+ public boolean isIOS() {
+ return browserDetails.isIOS();
+ }
+
+ /**
+ * Checks if the browser is run on Android
+ *
+ * @return true if the browser is run on Android, false otherwise
+ */
+ public boolean isAndroid() {
+ return browserDetails.isAndroid();
+ }
+
+ /**
+ * Checks if the browser is capable of handling scrolling natively or if a
+ * touch scroll helper is needed for scrolling.
+ *
+ * @return true if browser needs a touch scroll helper, false if the browser
+ * can handle scrolling natively
+ */
+ public boolean requiresTouchScrollDelegate() {
+ if (!isTouchDevice()) {
+ return false;
+ }
+ if (isAndroid() && isWebkit() && getWebkitVersion() >= 534) {
+ return false;
+ }
+ // Cannot enable native touch scrolling on iOS 5 until #8792 is resolved
+ // if (isIOS() && isWebkit() && getWebkitVersion() >= 534) {
+ // return false;
+ // }
+ return true;
+ }
+
+ /**
+ * Tests if this is an Android devices with a broken scrollTop
+ * implementation
+ *
+ * @return true if scrollTop cannot be trusted on this device, false
+ * otherwise
+ */
+ public boolean isAndroidWithBrokenScrollTop() {
+ return isAndroid()
+ && (getOperatingSystemMajorVersion() == 3 || getOperatingSystemMajorVersion() == 4);
+ }
+
+ private int getOperatingSystemMajorVersion() {
+ return browserDetails.getOperatingSystemMajorVersion();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/CSSRule.java b/client/src/com/vaadin/terminal/gwt/client/CSSRule.java
new file mode 100644
index 0000000000..c36b0611e8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/CSSRule.java
@@ -0,0 +1,120 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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;
+
+ /**
+ *
+ * @param selector
+ * the CSS selector to search for in the stylesheets
+ * @param deep
+ * should the search follow any @import statements?
+ */
+ public CSSRule(final String selector, final boolean deep) {
+ this.selector = selector;
+ fetchRule(selector, deep);
+ }
+
+ // TODO how to find the right LINK-element? We should probably give the
+ // stylesheet a name.
+ private native void fetchRule(final String selector, final boolean deep)
+ /*-{
+ var sheets = $doc.styleSheets;
+ for(var i = 0; i < sheets.length; i++) {
+ var sheet = sheets[i];
+ if(sheet.href && sheet.href.indexOf("VAADIN/themes")>-1) {
+ // $entry not needed as function is not exported
+ this.@com.vaadin.terminal.gwt.client.CSSRule::rules = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(sheet, selector, deep);
+ return;
+ }
+ }
+ this.@com.vaadin.terminal.gwt.client.CSSRule::rules = [];
+ }-*/;
+
+ /*
+ * 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(
+ final JavaScriptObject sheet, final String selector,
+ final boolean deep)
+ /*-{
+ if(!$doc.styleSheets)
+ return null;
+
+ selector = selector.toLowerCase();
+
+ var allMatches = [];
+
+ // IE handles imported sheet differently
+ if(deep && sheet.imports && sheet.imports.length > 0) {
+ for(var i=0; i < sheet.imports.length; i++) {
+ // $entry not needed as function is not exported
+ var imports = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(sheet.imports[i], selector, deep);
+ allMatches.concat(imports);
+ }
+ }
+
+ 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 == 1 || sheet.imports) {
+ 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
+ }
+ }
+ } else if(deep && r.type == 3) {
+ // Search @import stylesheet
+ // $entry not needed as function is not exported
+ var imports = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(r.styleSheet, selector, deep);
+ allMatches = allMatches.concat(imports);
+ }
+ }
+
+ return allMatches;
+ }-*/;
+
+ /**
+ * Returns a specific property value from this CSS rule.
+ *
+ * @param propertyName
+ * camelCase CSS property name
+ * @return the value of the property as a String
+ */
+ public native String getPropertyValue(final String propertyName)
+ /*-{
+ var j = this.@com.vaadin.terminal.gwt.client.CSSRule::rules.length;
+ for(var i=0; i<j; i++) {
+ // $entry not needed as function is not exported
+ 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/client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java b/client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java
new file mode 100644
index 0000000000..002c73343a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+
+@Deprecated
+public class ClientExceptionHandler {
+
+ public static void displayError(Throwable e) {
+ displayError(e.getClass().getName() + ": " + e.getMessage());
+
+ GWT.log(e.getMessage(), e);
+ }
+
+ @Deprecated
+ public static void displayError(String msg) {
+ VConsole.error(msg);
+ GWT.log(msg);
+ }
+
+ @Deprecated
+ public static void displayError(String msg, Throwable e) {
+ displayError(msg);
+ displayError(e);
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java
new file mode 100644
index 0000000000..e57a188b47
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ComponentConnector.java
@@ -0,0 +1,120 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ComponentState;
+
+/**
+ * An interface used by client-side widgets or paintable parts to receive
+ * updates from the corresponding server-side components in the form of
+ * {@link UIDL}.
+ *
+ * Updates can be sent back to the server using the
+ * {@link ApplicationConnection#updateVariable()} methods.
+ */
+public interface ComponentConnector extends ServerConnector {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.VPaintable#getState()
+ */
+ @Override
+ public ComponentState getState();
+
+ /**
+ * Returns the widget for this {@link ComponentConnector}
+ */
+ public Widget getWidget();
+
+ public LayoutManager getLayoutManager();
+
+ /**
+ * Returns <code>true</code> if the width of this paintable is currently
+ * undefined. If the width is undefined, the actual width of the paintable
+ * is defined by its contents.
+ *
+ * @return <code>true</code> if the width is undefined, else
+ * <code>false</code>
+ */
+ public boolean isUndefinedWidth();
+
+ /**
+ * Returns <code>true</code> if the height of this paintable is currently
+ * undefined. If the height is undefined, the actual height of the paintable
+ * is defined by its contents.
+ *
+ * @return <code>true</code> if the height is undefined, else
+ * <code>false</code>
+ */
+ public boolean isUndefinedHeight();
+
+ /**
+ * Returns <code>true</code> if the width of this paintable is currently
+ * relative. If the width is relative, the actual width of the paintable is
+ * a percentage of the size allocated to it by its parent.
+ *
+ * @return <code>true</code> if the width is undefined, else
+ * <code>false</code>
+ */
+ public boolean isRelativeWidth();
+
+ /**
+ * Returns <code>true</code> if the height of this paintable is currently
+ * relative. If the height is relative, the actual height of the paintable
+ * is a percentage of the size allocated to it by its parent.
+ *
+ * @return <code>true</code> if the width is undefined, else
+ * <code>false</code>
+ */
+ public boolean isRelativeHeight();
+
+ /**
+ * Checks if the connector is read only.
+ *
+ * @deprecated This belongs in AbstractFieldConnector, see #8514
+ * @return true
+ */
+ @Deprecated
+ public boolean isReadOnly();
+
+ public boolean hasEventListener(String eventIdentifier);
+
+ /**
+ * Return true if parent handles caption, false if the paintable handles the
+ * caption itself.
+ *
+ * <p>
+ * This should always return true and all components should let the parent
+ * handle the caption and use other attributes for internal texts in the
+ * component
+ * </p>
+ *
+ * @return true if caption handling is delegated to the parent, false if
+ * parent should not be allowed to render caption
+ */
+ public boolean delegateCaptionHandling();
+
+ /**
+ * Sets the enabled state of the widget associated to this connector.
+ *
+ * @param widgetEnabled
+ * true if the widget should be enabled, false otherwise
+ */
+ public void setWidgetEnabled(boolean widgetEnabled);
+
+ /**
+ * Gets the tooltip info for the given element.
+ *
+ * @param element
+ * The element to lookup a tooltip for
+ * @return The tooltip for the element or null if no tooltip is defined for
+ * this element.
+ */
+ public TooltipInfo getTooltipInfo(Element element);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java b/client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java
new file mode 100644
index 0000000000..08ce3d31dc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ComponentContainerConnector.java
@@ -0,0 +1,74 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.List;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
+
+/**
+ * An interface used by client-side connectors whose widget is a component
+ * container (implements {@link HasWidgets}).
+ */
+public interface ComponentContainerConnector extends ServerConnector {
+
+ /**
+ * 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 connector
+ * Child component for which service is requested.
+ */
+ void updateCaption(ComponentConnector connector);
+
+ /**
+ * Returns the children for this connector.
+ * <p>
+ * The children for this connector are defined as all
+ * {@link ComponentConnector}s whose parent is this
+ * {@link ComponentContainerConnector}.
+ * </p>
+ *
+ * @return A collection of children for this connector. An empty collection
+ * if there are no children. Never returns null.
+ */
+ public List<ComponentConnector> getChildComponents();
+
+ /**
+ * Sets the children for this connector. This method should only be called
+ * by the framework to ensure that the connector hierarchy on the client
+ * side and the server side are in sync.
+ * <p>
+ * Note that calling this method does not call
+ * {@link ConnectorHierarchyChangeHandler#onConnectorHierarchyChange(ConnectorHierarchyChangeEvent)}
+ * . The event method is called only when the hierarchy has been updated for
+ * all connectors.
+ *
+ * @param children
+ * The new child connectors
+ */
+ public void setChildComponents(List<ComponentConnector> children);
+
+ /**
+ * Adds a handler that is called whenever the child hierarchy of this
+ * connector has been updated by the server.
+ *
+ * @param handler
+ * The handler that should be added.
+ * @return A handler registration reference that can be used to unregister
+ * the handler
+ */
+ public HandlerRegistration addConnectorHierarchyChangeHandler(
+ ConnectorHierarchyChangeHandler handler);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java b/client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java
new file mode 100644
index 0000000000..686cb640a4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ComponentDetail.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+
+class ComponentDetail {
+
+ private TooltipInfo tooltipInfo = new TooltipInfo();
+
+ public ComponentDetail() {
+
+ }
+
+ /**
+ * Returns a TooltipInfo assosiated with Component. If element is given,
+ * returns an additional TooltipInfo.
+ *
+ * @param key
+ * @return the tooltipInfo
+ */
+ public TooltipInfo getTooltipInfo(Object key) {
+ if (key == null) {
+ return tooltipInfo;
+ } else {
+ if (additionalTooltips != null) {
+ return additionalTooltips.get(key);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @param tooltipInfo
+ * the tooltipInfo to set
+ */
+ public void setTooltipInfo(TooltipInfo tooltipInfo) {
+ this.tooltipInfo = tooltipInfo;
+ }
+
+ private HashMap<Object, TooltipInfo> additionalTooltips;
+
+ public void putAdditionalTooltip(Object key, TooltipInfo tooltip) {
+ if (tooltip == null && additionalTooltips != null) {
+ additionalTooltips.remove(key);
+ } else {
+ if (additionalTooltips == null) {
+ additionalTooltips = new HashMap<Object, TooltipInfo>();
+ }
+ additionalTooltips.put(key, tooltip);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java b/client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java
new file mode 100644
index 0000000000..dfbcf9d38b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java
@@ -0,0 +1,76 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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) {
+ // $entry not needed as function is not exported
+ 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/client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java b/client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java
new file mode 100644
index 0000000000..28252a9efb
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ComponentLocator.java
@@ -0,0 +1,610 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+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.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.gridlayout.VGridLayout;
+import com.vaadin.terminal.gwt.client.ui.orderedlayout.VMeasuringOrderedLayout;
+import com.vaadin.terminal.gwt.client.ui.root.VRoot;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheetPanel;
+import com.vaadin.terminal.gwt.client.ui.window.VWindow;
+import com.vaadin.terminal.gwt.client.ui.window.WindowConnector;
+
+/**
+ * ComponentLocator provides methods for generating a String locator for a given
+ * DOM element and for locating a DOM element using a String locator.
+ */
+public class ComponentLocator {
+
+ /**
+ * Separator used in the String locator between a parent and a child widget.
+ */
+ private static final String PARENTCHILD_SEPARATOR = "/";
+
+ /**
+ * Separator used in the String locator between the part identifying the
+ * containing widget and the part identifying the target element within the
+ * widget.
+ */
+ private static final String SUBPART_SEPARATOR = "#";
+
+ /**
+ * String that identifies the root panel when appearing first in the String
+ * locator.
+ */
+ private static final String ROOT_ID = "Root";
+
+ /**
+ * Reference to ApplicationConnection instance.
+ */
+ private ApplicationConnection client;
+
+ /**
+ * Construct a ComponentLocator for the given ApplicationConnection.
+ *
+ * @param client
+ * ApplicationConnection instance for the application.
+ */
+ public ComponentLocator(ApplicationConnection client) {
+ this.client = client;
+ }
+
+ /**
+ * Generates a String locator which uniquely identifies the target element.
+ * The {@link #getElementByPath(String)} method can be used for the inverse
+ * operation, i.e. locating an element based on the return value from this
+ * method.
+ * <p>
+ * Note that getElementByPath(getPathForElement(element)) == element is not
+ * always true as {@link #getPathForElement(Element)} can return a path to
+ * another element if the widget determines an action on the other element
+ * will give the same result as the action on the target element.
+ * </p>
+ *
+ * @since 5.4
+ * @param targetElement
+ * The element to generate a path for.
+ * @return A String locator that identifies the target element or null if a
+ * String locator could not be created.
+ */
+ public String getPathForElement(Element targetElement) {
+ String pid = null;
+
+ Element e = targetElement;
+
+ while (true) {
+ pid = ConnectorMap.get(client).getConnectorId(e);
+ if (pid != null) {
+ break;
+ }
+
+ e = DOM.getParent(e);
+ if (e == null) {
+ break;
+ }
+ }
+
+ Widget w = null;
+ if (pid != null) {
+ // If we found a Paintable then we use that as reference. We should
+ // find the Paintable for all but very special cases (like
+ // overlays).
+ w = ((ComponentConnector) ConnectorMap.get(client)
+ .getConnector(pid)).getWidget();
+
+ /*
+ * Still if the Paintable contains a widget that implements
+ * SubPartAware, we want to use that as a reference
+ */
+ Widget targetParent = findParentWidget(targetElement, w);
+ while (targetParent != w && targetParent != null) {
+ if (targetParent instanceof SubPartAware) {
+ /*
+ * The targetParent widget is a child of the Paintable and
+ * the first parent (of the targetElement) that implements
+ * SubPartAware
+ */
+ w = targetParent;
+ break;
+ }
+ targetParent = targetParent.getParent();
+ }
+ }
+ if (w == null) {
+ // Check if the element is part of a widget that is attached
+ // directly to the root panel
+ RootPanel rootPanel = RootPanel.get();
+ int rootWidgetCount = rootPanel.getWidgetCount();
+ for (int i = 0; i < rootWidgetCount; i++) {
+ Widget rootWidget = rootPanel.getWidget(i);
+ if (rootWidget.getElement().isOrHasChild(targetElement)) {
+ // The target element is contained by this root widget
+ w = findParentWidget(targetElement, rootWidget);
+ break;
+ }
+ }
+ if (w != null) {
+ // We found a widget but we should still see if we find a
+ // SubPartAware implementor (we cannot find the Paintable as
+ // there is no link from VOverlay to its paintable/owner).
+ Widget subPartAwareWidget = findSubPartAwareParentWidget(w);
+ if (subPartAwareWidget != null) {
+ w = subPartAwareWidget;
+ }
+ }
+ }
+
+ if (w == null) {
+ // Containing widget not found
+ return null;
+ }
+
+ // Determine the path for the target widget
+ String path = getPathForWidget(w);
+ if (path == null) {
+ /*
+ * No path could be determined for the target widget. Cannot create
+ * a locator string.
+ */
+ return null;
+ }
+
+ if (w.getElement() == targetElement) {
+ /*
+ * We are done if the target element is the root of the target
+ * widget.
+ */
+ return path;
+ } else if (w instanceof SubPartAware) {
+ /*
+ * If the widget can provide an identifier for the targetElement we
+ * let it do that
+ */
+ String elementLocator = ((SubPartAware) w)
+ .getSubPartName(targetElement);
+ if (elementLocator != null) {
+ return path + SUBPART_SEPARATOR + elementLocator;
+ }
+ }
+ /*
+ * If everything else fails we use the DOM path to identify the target
+ * element
+ */
+ return path + getDOMPathForElement(targetElement, w.getElement());
+ }
+
+ /**
+ * Finds the first widget in the hierarchy (moving upwards) that implements
+ * SubPartAware. Returns the SubPartAware implementor or null if none is
+ * found.
+ *
+ * @param w
+ * The widget to start from. This is returned if it implements
+ * SubPartAware.
+ * @return The first widget (upwards in hierarchy) that implements
+ * SubPartAware or null
+ */
+ private Widget findSubPartAwareParentWidget(Widget w) {
+
+ while (w != null) {
+ if (w instanceof SubPartAware) {
+ return w;
+ }
+ w = w.getParent();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first widget found when going from {@code targetElement}
+ * upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a
+ * parent of {@code targetElement}.
+ *
+ * @param targetElement
+ * @param ancestorWidget
+ * @return The widget whose root element is a parent of
+ * {@code targetElement}.
+ */
+ private Widget findParentWidget(Element targetElement, Widget ancestorWidget) {
+ /*
+ * As we cannot resolve Widgets from the element we start from the
+ * widget and move downwards to the correct child widget, as long as we
+ * find one.
+ */
+ if (ancestorWidget instanceof HasWidgets) {
+ for (Widget w : ((HasWidgets) ancestorWidget)) {
+ if (w.getElement().isOrHasChild(targetElement)) {
+ return findParentWidget(targetElement, w);
+ }
+ }
+ }
+
+ // No children found, this is it
+ return ancestorWidget;
+ }
+
+ /**
+ * Locates an element based on a DOM path and a base element.
+ *
+ * @param baseElement
+ * The base element which the path is relative to
+ * @param path
+ * String locator (consisting of domChild[x] parts) that
+ * identifies the element
+ * @return The element identified by path, relative to baseElement or null
+ * if the element could not be found.
+ */
+ 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) {
+ return null;
+ }
+ }
+ }
+
+ return element;
+ }
+
+ /**
+ * Generates a String locator using domChild[x] parts for the element
+ * relative to the baseElement.
+ *
+ * @param element
+ * The target element
+ * @param baseElement
+ * The starting point for the locator. The generated path is
+ * relative to this element.
+ * @return A String locator that can be used to locate the target element
+ * using {@link #getElementByDOMPath(Element, String)} or null if
+ * the locator String cannot be created.
+ */
+ private String getDOMPathForElement(Element element, Element baseElement) {
+ Element e = element;
+ String path = "";
+ while (true) {
+ Element parent = DOM.getParent(e);
+ if (parent == null) {
+ return null;
+ }
+
+ 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 null;
+ }
+
+ path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]"
+ + path;
+
+ if (parent == baseElement) {
+ break;
+ }
+
+ e = parent;
+ }
+
+ return path;
+ }
+
+ /**
+ * Locates an element using a String locator (path) which identifies a DOM
+ * element. The {@link #getPathForElement(Element)} method can be used for
+ * the inverse operation, i.e. generating a string expression for a DOM
+ * element.
+ *
+ * @since 5.4
+ * @param path
+ * The String locater which identifies the target element.
+ * @return The DOM element identified by {@code path} or null if the element
+ * could not be located.
+ */
+ public Element getElementByPath(String path) {
+ /*
+ * Path is of type "targetWidgetPath#componentPart" or
+ * "targetWidgetPath".
+ */
+ String parts[] = path.split(SUBPART_SEPARATOR, 2);
+ String widgetPath = parts[0];
+ Widget w = getWidgetFromPath(widgetPath);
+ if (w == null || !Util.isAttachedAndDisplayed(w)) {
+ 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) {
+ return ((SubPartAware) w).getSubPartElement(parts[1]);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a locator String for the given widget. The path can be used to
+ * locate the widget using {@link #getWidgetFromPath(String)}.
+ *
+ * Returns null if no path can be determined for the widget or if the widget
+ * is null.
+ *
+ * @param w
+ * The target widget
+ * @return A String locator for the widget
+ */
+ private String getPathForWidget(Widget w) {
+ if (w == null) {
+ return null;
+ }
+
+ if (w instanceof VRoot) {
+ return "";
+ } else if (w instanceof VWindow) {
+ Connector windowConnector = ConnectorMap.get(client)
+ .getConnector(w);
+ List<WindowConnector> subWindowList = client.getRootConnector()
+ .getSubWindows();
+ int indexOfSubWindow = subWindowList.indexOf(windowConnector);
+ return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]";
+ } else if (w instanceof RootPanel) {
+ return ROOT_ID;
+ }
+
+ Widget parent = w.getParent();
+
+ String basePath = getPathForWidget(parent);
+ if (basePath == null) {
+ return null;
+ }
+ String simpleName = Util.getSimpleName(w);
+
+ /*
+ * Check if the parent implements Iterable. At least VPopupView does not
+ * implement HasWdgets so we cannot check for that.
+ */
+ if (!(parent instanceof Iterable<?>)) {
+ // Parent does not implement Iterable so we cannot find out which
+ // child this is
+ return null;
+ }
+
+ Iterator<?> i = ((Iterable<?>) 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 null;
+ }
+
+ /**
+ * Locates the widget based on a String locator.
+ *
+ * @param path
+ * The String locator that identifies the widget.
+ * @return The Widget identified by the String locator or null if the widget
+ * could not be identified.
+ */
+ private Widget getWidgetFromPath(String path) {
+ Widget w = null;
+ String parts[] = path.split(PARENTCHILD_SEPARATOR);
+
+ for (int i = 0; i < parts.length; i++) {
+ String part = parts[i];
+
+ if (part.equals(ROOT_ID)) {
+ w = RootPanel.get();
+ } else if (part.equals("")) {
+ w = client.getRootConnector().getWidget();
+ } else if (w == null) {
+ String id = part;
+ // Must be old static pid (PID_S*)
+ ServerConnector connector = ConnectorMap.get(client)
+ .getConnector(id);
+ if (connector == null) {
+ // Lookup by debugId
+ // TODO Optimize this
+ connector = findConnectorById(client.getRootConnector(),
+ id.substring(5));
+ }
+
+ if (connector instanceof ComponentConnector) {
+ w = ((ComponentConnector) connector).getWidget();
+ } else {
+ // Not found
+ return null;
+ }
+ } else if (part.startsWith("domChild[")) {
+ // The target widget has been found and the rest identifies the
+ // element
+ break;
+ } else if (w instanceof Iterable) {
+ // W identifies a widget that contains other widgets, as it
+ // should. Try to locate the child
+ Iterable<?> parent = (Iterable<?>) w;
+
+ // Part is of type "VVerticalLayout[0]", split this into
+ // VVerticalLayout and 0
+ String[] split = part.split("\\[", 2);
+ String widgetClassName = split[0];
+ String indexString = split[1];
+ int widgetPosition = Integer.parseInt(indexString.substring(0,
+ indexString.length() - 1));
+
+ // AbsolutePanel in GridLayout has been removed -> skip it
+ if (w instanceof VGridLayout
+ && "AbsolutePanel".equals(widgetClassName)) {
+ continue;
+ }
+
+ if (w instanceof VTabsheetPanel && widgetPosition != 0) {
+ // TabSheetPanel now only contains 1 connector => the index
+ // is always 0 which indicates the widget in the active tab
+ widgetPosition = 0;
+ }
+
+ /*
+ * The new grid and ordered layotus do not contain
+ * ChildComponentContainer widgets. This is instead simulated by
+ * constructing a path step that would find the desired widget
+ * from the layout and injecting it as the next search step
+ * (which would originally have found the widget inside the
+ * ChildComponentContainer)
+ */
+ if ((w instanceof VMeasuringOrderedLayout || w instanceof VGridLayout)
+ && "ChildComponentContainer".equals(widgetClassName)
+ && i + 1 < parts.length) {
+
+ HasWidgets layout = (HasWidgets) w;
+
+ String nextPart = parts[i + 1];
+ String[] nextSplit = nextPart.split("\\[", 2);
+ String nextWidgetClassName = nextSplit[0];
+
+ // Find the n:th child and count the number of children with
+ // the same type before it
+ int nextIndex = 0;
+ for (Widget child : layout) {
+ boolean matchingType = nextWidgetClassName.equals(Util
+ .getSimpleName(child));
+ if (matchingType && widgetPosition == 0) {
+ // This is the n:th child that we looked for
+ break;
+ } else if (widgetPosition < 0) {
+ // Error if we're past the desired position without
+ // a match
+ return null;
+ } else if (matchingType) {
+ // If this was another child of the expected type,
+ // increase the count for the next step
+ nextIndex++;
+ }
+
+ // Don't count captions
+ if (!(child instanceof VCaption)) {
+ widgetPosition--;
+ }
+ }
+
+ // Advance to the next step, this time checking for the
+ // actual child widget
+ parts[i + 1] = nextWidgetClassName + '[' + nextIndex + ']';
+ continue;
+ }
+
+ // Locate the child
+ Iterator<? extends Widget> iterator;
+
+ /*
+ * VWindow and VContextMenu workarounds for backwards
+ * compatibility
+ */
+ if (widgetClassName.equals("VWindow")) {
+ List<WindowConnector> windows = client.getRootConnector()
+ .getSubWindows();
+ List<VWindow> windowWidgets = new ArrayList<VWindow>(
+ windows.size());
+ for (WindowConnector wc : windows) {
+ windowWidgets.add(wc.getWidget());
+ }
+ iterator = windowWidgets.iterator();
+ } else if (widgetClassName.equals("VContextMenu")) {
+ return client.getContextMenu();
+ } else {
+ iterator = (Iterator<? extends Widget>) parent.iterator();
+ }
+
+ boolean ok = false;
+
+ // Find the widgetPosition:th child of type "widgetClassName"
+ while (iterator.hasNext()) {
+
+ Widget child = iterator.next();
+ String simpleName2 = Util.getSimpleName(child);
+
+ if (widgetClassName.equals(simpleName2)) {
+ if (widgetPosition == 0) {
+ w = child;
+ ok = true;
+ break;
+ }
+ widgetPosition--;
+ }
+ }
+
+ if (!ok) {
+ // Did not find the child
+ return null;
+ }
+ } else {
+ // W identifies something that is not a "HasWidgets". This
+ // should not happen as all widget containers should implement
+ // HasWidgets.
+ return null;
+ }
+ }
+
+ return w;
+ }
+
+ private ServerConnector findConnectorById(ServerConnector root, String id) {
+ SharedState state = root.getState();
+ if (state instanceof ComponentState
+ && id.equals(((ComponentState) state).getDebugId())) {
+ return root;
+ }
+ for (ServerConnector child : root.getChildren()) {
+ ServerConnector found = findConnectorById(child, id);
+ if (found != null) {
+ return found;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ComputedStyle.java b/client/src/com/vaadin/terminal/gwt/client/ComputedStyle.java
new file mode 100644
index 0000000000..29b02b4dde
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ComputedStyle.java
@@ -0,0 +1,186 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Element;
+
+public class ComputedStyle {
+
+ protected final JavaScriptObject computedStyle;
+ private final Element elem;
+
+ /**
+ * Gets this element's computed style object which can be used to gather
+ * information about the current state of the rendered node.
+ * <p>
+ * Note that this method is expensive. Wherever possible, reuse the returned
+ * object.
+ *
+ * @param elem
+ * the element
+ * @return the computed style
+ */
+ public ComputedStyle(Element elem) {
+ computedStyle = getComputedStyle(elem);
+ this.elem = elem;
+ }
+
+ private static native JavaScriptObject getComputedStyle(Element elem)
+ /*-{
+ if(elem.nodeType != 1) {
+ return {};
+ }
+
+ if($wnd.document.defaultView && $wnd.document.defaultView.getComputedStyle) {
+ return $wnd.document.defaultView.getComputedStyle(elem, null);
+ }
+
+ if(elem.currentStyle) {
+ return elem.currentStyle;
+ }
+ }-*/;
+
+ /**
+ *
+ * @param name
+ * name of the CSS property in camelCase
+ * @return the value of the property, normalized for across browsers (each
+ * browser returns pixel values whenever possible).
+ */
+ public final native String getProperty(String name)
+ /*-{
+ var cs = this.@com.vaadin.terminal.gwt.client.ComputedStyle::computedStyle;
+ var elem = this.@com.vaadin.terminal.gwt.client.ComputedStyle::elem;
+
+ // Border values need to be checked separately. The width might have a
+ // meaningful value even if the border style is "none". In that case the
+ // value should be 0.
+ if(name.indexOf("border") > -1 && name.indexOf("Width") > -1) {
+ var borderStyleProp = name.substring(0,name.length-5) + "Style";
+ if(cs.getPropertyValue)
+ var borderStyle = cs.getPropertyValue(borderStyleProp);
+ else // IE
+ var borderStyle = cs[borderStyleProp];
+ if(borderStyle == "none")
+ return "0px";
+ }
+
+ if(cs.getPropertyValue) {
+
+ // Convert name to dashed format
+ name = name.replace(/([A-Z])/g, "-$1").toLowerCase();
+ var ret = cs.getPropertyValue(name);
+
+ } else {
+
+ var ret = cs[name];
+ var style = elem.style;
+
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+ // If we're not dealing with a regular pixel number
+ // but a number that has a weird ending, we need to convert it to pixels
+ if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
+ // Remember the original values
+ var left = style.left, rsLeft = elem.runtimeStyle.left;
+
+ // Put in the new values to get a computed value out
+ elem.runtimeStyle.left = cs.left;
+ style.left = ret || 0;
+ ret = style.pixelLeft + "px";
+
+ // Revert the changed values
+ style.left = left;
+ elem.runtimeStyle.left = rsLeft;
+ }
+
+ }
+
+ // Normalize margin values. This is not totally valid, but in most cases
+ // it is what the user wants to know.
+ if(name.indexOf("margin") > -1 && ret == "auto") {
+ return "0px";
+ }
+
+ // Some browsers return undefined width and height values as "auto", so
+ // we need to retrieve those ourselves.
+ if (name == "width" && ret == "auto") {
+ ret = elem.clientWidth + "px";
+ } else if (name == "height" && ret == "auto") {
+ ret = elem.clientHeight + "px";
+ }
+
+ return ret;
+
+ }-*/;
+
+ public final int getIntProperty(String name) {
+ Integer parsed = parseInt(getProperty(name));
+ if (parsed != null) {
+ return parsed.intValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Get current margin values from the DOM. The array order is the default
+ * CSS order: top, right, bottom, left.
+ */
+ public final int[] getMargin() {
+ int[] margin = { 0, 0, 0, 0 };
+ margin[0] = getIntProperty("marginTop");
+ margin[1] = getIntProperty("marginRight");
+ margin[2] = getIntProperty("marginBottom");
+ margin[3] = getIntProperty("marginLeft");
+ return margin;
+ }
+
+ /**
+ * Get current padding values from the DOM. The array order is the default
+ * CSS order: top, right, bottom, left.
+ */
+ public final int[] getPadding() {
+ int[] padding = { 0, 0, 0, 0 };
+ padding[0] = getIntProperty("paddingTop");
+ padding[1] = getIntProperty("paddingRight");
+ padding[2] = getIntProperty("paddingBottom");
+ padding[3] = getIntProperty("paddingLeft");
+ return padding;
+ }
+
+ /**
+ * Get current border values from the DOM. The array order is the default
+ * CSS order: top, right, bottom, left.
+ */
+ public final int[] getBorder() {
+ int[] border = { 0, 0, 0, 0 };
+ border[0] = getIntProperty("borderTopWidth");
+ border[1] = getIntProperty("borderRightWidth");
+ border[2] = getIntProperty("borderBottomWidth");
+ border[3] = getIntProperty("borderLeftWidth");
+ return border;
+ }
+
+ /**
+ * Takes a String value e.g. "12px" and parses that to int 12.
+ *
+ * @param String
+ * a value starting with a number
+ * @return int the value from the string before any non-numeric characters.
+ * If the value cannot be parsed to a number, returns
+ * <code>null</code>.
+ */
+ public static native Integer parseInt(final String value)
+ /*-{
+ var number = parseInt(value, 10);
+ if (isNaN(number))
+ return null;
+ else
+ // $entry not needed as function is not exported
+ return @java.lang.Integer::valueOf(I)(number);
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java b/client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java
new file mode 100644
index 0000000000..aa41caf75d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ConnectorHierarchyChangeEvent.java
@@ -0,0 +1,97 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
+import com.vaadin.terminal.gwt.client.communication.AbstractServerConnectorEvent;
+
+/**
+ * Event for containing data related to a change in the {@link ServerConnector}
+ * hierarchy. A {@link ConnectorHierarchyChangedEvent} is fired when an update
+ * from the server has been fully processed and all hierarchy updates have been
+ * completed.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public class ConnectorHierarchyChangeEvent extends
+ AbstractServerConnectorEvent<ConnectorHierarchyChangeHandler> {
+ /**
+ * Type of this event, used by the event bus.
+ */
+ public static final Type<ConnectorHierarchyChangeHandler> TYPE = new Type<ConnectorHierarchyChangeHandler>();
+
+ List<ComponentConnector> oldChildren;
+ private ComponentContainerConnector parent;
+
+ public ConnectorHierarchyChangeEvent() {
+ }
+
+ /**
+ * Returns a collection of the old children for the connector. This was the
+ * state before the update was received from the server.
+ *
+ * @return A collection of old child connectors. Never returns null.
+ */
+ public List<ComponentConnector> getOldChildren() {
+ return oldChildren;
+ }
+
+ /**
+ * Sets the collection of the old children for the connector.
+ *
+ * @param oldChildren
+ * The old child connectors. Must not be null.
+ */
+ public void setOldChildren(List<ComponentConnector> oldChildren) {
+ this.oldChildren = oldChildren;
+ }
+
+ /**
+ * Returns the {@link ComponentContainerConnector} for which this event
+ * occurred.
+ *
+ * @return The {@link ComponentContainerConnector} whose child collection
+ * has changed. Never returns null.
+ */
+ public ComponentContainerConnector getParent() {
+ return parent;
+ }
+
+ /**
+ * Sets the {@link ComponentContainerConnector} for which this event
+ * occurred.
+ *
+ * @param The
+ * {@link ComponentContainerConnector} whose child collection has
+ * changed.
+ */
+ public void setParent(ComponentContainerConnector parent) {
+ this.parent = parent;
+ }
+
+ public interface ConnectorHierarchyChangeHandler extends Serializable,
+ EventHandler {
+ public void onConnectorHierarchyChange(
+ ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent);
+ }
+
+ @Override
+ public void dispatch(ConnectorHierarchyChangeHandler handler) {
+ handler.onConnectorHierarchyChange(this);
+ }
+
+ @Override
+ public GwtEvent.Type<ConnectorHierarchyChangeHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java b/client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java
new file mode 100644
index 0000000000..8bc4a4aacf
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ConnectorMap.java
@@ -0,0 +1,219 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+
+public class ConnectorMap {
+
+ private Map<String, ServerConnector> idToConnector = new HashMap<String, ServerConnector>();
+
+ public static ConnectorMap get(ApplicationConnection applicationConnection) {
+ return applicationConnection.getConnectorMap();
+ }
+
+ @Deprecated
+ private final ComponentDetailMap idToComponentDetail = ComponentDetailMap
+ .create();
+
+ /**
+ * Returns a {@link ServerConnector} by its id
+ *
+ * @param id
+ * The connector id
+ * @return A connector or null if a connector with the given id has not been
+ * registered
+ */
+ public ServerConnector getConnector(String connectorId) {
+ return idToConnector.get(connectorId);
+ }
+
+ /**
+ * Returns a {@link ComponentConnector} element by its root element
+ *
+ * @param element
+ * Root element of the {@link ComponentConnector}
+ * @return A connector or null if a connector with the given id has not been
+ * registered
+ */
+ public ComponentConnector getConnector(Element element) {
+ return (ComponentConnector) getConnector(getConnectorId(element));
+ }
+
+ /**
+ * FIXME: What does this even do and why?
+ *
+ * @param pid
+ * @return
+ */
+ public boolean isDragAndDropPaintable(String pid) {
+ return (pid.startsWith("DD"));
+ }
+
+ /**
+ * Checks if a connector with the given id has been registered.
+ *
+ * @param connectorId
+ * The id to check for
+ * @return true if a connector has been registered with the given id, false
+ * otherwise
+ */
+ public boolean hasConnector(String connectorId) {
+ return idToConnector.containsKey(connectorId);
+ }
+
+ /**
+ * Removes all registered connectors
+ */
+ public void clear() {
+ idToConnector.clear();
+ idToComponentDetail.clear();
+ }
+
+ /**
+ * Retrieves the connector whose widget matches the parameter.
+ *
+ * @param widget
+ * The widget
+ * @return A connector with {@literal widget} as its root widget or null if
+ * no connector was found
+ */
+ public ComponentConnector getConnector(Widget widget) {
+ return getConnector(widget.getElement());
+ }
+
+ public void registerConnector(String id, ServerConnector connector) {
+ ComponentDetail componentDetail = GWT.create(ComponentDetail.class);
+ idToComponentDetail.put(id, componentDetail);
+ idToConnector.put(id, connector);
+ if (connector instanceof ComponentConnector) {
+ ComponentConnector pw = (ComponentConnector) connector;
+ setConnectorId(pw.getWidget().getElement(), id);
+ }
+ }
+
+ private native void setConnectorId(Element el, String id)
+ /*-{
+ el.tkPid = id;
+ }-*/;
+
+ /**
+ * Gets the connector id using a DOM element - the element should be the
+ * root element for a connector, otherwise no id will be found. Use
+ * {@link #getConnectorId(ServerConnector)} instead whenever possible.
+ *
+ * @see #getConnectorId(ServerConnector)
+ * @param el
+ * element of the connector whose id is desired
+ * @return the id of the element's connector, if it's a connector
+ */
+ native String getConnectorId(Element el)
+ /*-{
+ return el.tkPid;
+ }-*/;
+
+ /**
+ * Gets the main element for the connector with the given id. The reverse of
+ * {@link #getConnectorId(Element)}.
+ *
+ * @param connectorId
+ * the id of the widget whose element is desired
+ * @return the element for the connector corresponding to the id
+ */
+ public Element getElement(String connectorId) {
+ ServerConnector p = getConnector(connectorId);
+ if (p instanceof ComponentConnector) {
+ return ((ComponentConnector) p).getWidget().getElement();
+ }
+
+ return null;
+ }
+
+ /**
+ * Unregisters the given connector; always use after removing a connector.
+ * This method does not remove the connector from the DOM, but marks the
+ * connector so that ApplicationConnection may clean up its references to
+ * it. Removing the widget from DOM is component containers responsibility.
+ *
+ * @param connector
+ * the connector to remove
+ */
+ public void unregisterConnector(ServerConnector connector) {
+ if (connector == null) {
+ VConsole.error("Trying to unregister null connector");
+ return;
+ }
+
+ String connectorId = connector.getConnectorId();
+
+ idToComponentDetail.remove(connectorId);
+ idToConnector.remove(connectorId);
+ connector.onUnregister();
+
+ for (ServerConnector child : connector.getChildren()) {
+ if (child.getParent() == connector) {
+ /*
+ * Only unregister children that are actually connected to this
+ * parent. For instance when moving connectors from one layout
+ * to another and removing the first layout it will still
+ * contain references to its old children, which are now
+ * attached to another connector.
+ */
+ unregisterConnector(child);
+ }
+ }
+ }
+
+ /**
+ * Gets all registered {@link ComponentConnector} instances
+ *
+ * @return An array of all registered {@link ComponentConnector} instances
+ */
+ public ComponentConnector[] getComponentConnectors() {
+ ArrayList<ComponentConnector> result = new ArrayList<ComponentConnector>();
+
+ for (ServerConnector connector : getConnectors()) {
+ if (connector instanceof ComponentConnector) {
+ result.add((ComponentConnector) connector);
+ }
+ }
+
+ return result.toArray(new ComponentConnector[result.size()]);
+ }
+
+ @Deprecated
+ private ComponentDetail getComponentDetail(
+ ComponentConnector componentConnector) {
+ return idToComponentDetail.get(componentConnector.getConnectorId());
+ }
+
+ public int size() {
+ return idToConnector.size();
+ }
+
+ public Collection<? extends ServerConnector> getConnectors() {
+ return Collections.unmodifiableCollection(idToConnector.values());
+ }
+
+ /**
+ * Tests if the widget is the root widget of a {@link ComponentConnector}.
+ *
+ * @param widget
+ * The widget to test
+ * @return true if the widget is the root widget of a
+ * {@link ComponentConnector}, false otherwise
+ */
+ public boolean isConnector(Widget w) {
+ return getConnectorId(w.getElement()) != null;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/Console.java b/client/src/com/vaadin/terminal/gwt/client/Console.java
new file mode 100644
index 0000000000..64b2646201
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/Console.java
@@ -0,0 +1,32 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Set;
+
+public interface Console {
+
+ public abstract void log(String msg);
+
+ public abstract void log(Throwable e);
+
+ public abstract void error(Throwable e);
+
+ public abstract void error(String msg);
+
+ public abstract void printObject(Object msg);
+
+ public abstract void dirUIDL(ValueMap u, ApplicationConnection client);
+
+ public abstract void printLayoutProblems(ValueMap meta,
+ ApplicationConnection applicationConnection,
+ Set<ComponentConnector> zeroHeightComponents,
+ Set<ComponentConnector> zeroWidthComponents);
+
+ public abstract void setQuietMode(boolean quietDebugMode);
+
+ public abstract void init();
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java b/client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java
new file mode 100644
index 0000000000..569080d9ad
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java
@@ -0,0 +1,21 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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/client/src/com/vaadin/terminal/gwt/client/DateTimeService.java b/client/src/com/vaadin/terminal/gwt/client/DateTimeService.java
new file mode 100644
index 0000000000..45ba4a7452
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/DateTimeService.java
@@ -0,0 +1,439 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.LocaleInfo;
+import com.vaadin.terminal.gwt.client.ui.datefield.VDateField;
+
+/**
+ * This class provides date/time parsing services to all components on the
+ * client side.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+@SuppressWarnings("deprecation")
+public class DateTimeService {
+
+ 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) {
+ VConsole.error(e);
+ return null;
+ }
+ }
+
+ public String getShortMonth(int month) {
+ try {
+ return LocaleService.getShortMonthNames(currentLocale)[month];
+ } catch (final LocaleNotLoadedException e) {
+ VConsole.error(e);
+ return null;
+ }
+ }
+
+ public String getDay(int day) {
+ try {
+ return LocaleService.getDayNames(currentLocale)[day];
+ } catch (final LocaleNotLoadedException e) {
+ VConsole.error(e);
+ return null;
+ }
+ }
+
+ public String getShortDay(int day) {
+ try {
+ return LocaleService.getShortDayNames(currentLocale)[day];
+ } catch (final LocaleNotLoadedException e) {
+ VConsole.error(e);
+ return null;
+ }
+ }
+
+ public int getFirstDayOfWeek() {
+ try {
+ return LocaleService.getFirstDayOfWeek(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ VConsole.error(e);
+ return 0;
+ }
+ }
+
+ public boolean isTwelveHourClock() {
+ try {
+ return LocaleService.isTwelveHourClock(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ VConsole.error(e);
+ return false;
+ }
+ }
+
+ public String getClockDelimeter() {
+ try {
+ return LocaleService.getClockDelimiter(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ VConsole.error(e);
+ return ":";
+ }
+ }
+
+ private static final String[] DEFAULT_AMPM_STRINGS = { "AM", "PM" };
+
+ public String[] getAmPmStrings() {
+ try {
+ return LocaleService.getAmPmStrings(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ // TODO can this practically even happen? Should die instead?
+ VConsole.error("Locale not loaded, using fallback : AM/PM");
+ VConsole.error(e);
+ return DEFAULT_AMPM_STRINGS;
+ }
+ }
+
+ 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) {
+ VConsole.error("Locale not loaded, using fallback 0");
+ VConsole.error(e);
+ firstDay = 0;
+ }
+ int start = dateForFirstOfThisMonth.getDay() - firstDay;
+ if (start < 0) {
+ start = 6;
+ }
+ return start;
+ }
+
+ public static void setMilliseconds(Date date, int ms) {
+ date.setTime(date.getTime() / 1000 * 1000 + ms);
+ }
+
+ public static int getMilliseconds(Date date) {
+ if (date == null) {
+ return 0;
+ }
+
+ return (int) (date.getTime() - date.getTime() / 1000 * 1000);
+ }
+
+ 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 == VDateField.RESOLUTION_YEAR) {
+ return (start <= target && end >= target);
+ }
+ start += s.getMonth() * 100000000l;
+ end += e.getMonth() * 100000000l;
+ target += date.getMonth() * 100000000l;
+ if (resolution == VDateField.RESOLUTION_MONTH) {
+ return (start <= target && end >= target);
+ }
+ start += s.getDate() * 1000000l;
+ end += e.getDate() * 1000000l;
+ target += date.getDate() * 1000000l;
+ if (resolution == VDateField.RESOLUTION_DAY) {
+ return (start <= target && end >= target);
+ }
+ start += s.getHours() * 10000l;
+ end += e.getHours() * 10000l;
+ target += date.getHours() * 10000l;
+ if (resolution == VDateField.RESOLUTION_HOUR) {
+ return (start <= target && end >= target);
+ }
+ start += s.getMinutes() * 100l;
+ end += e.getMinutes() * 100l;
+ target += date.getMinutes() * 100l;
+ if (resolution == VDateField.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;
+ }
+
+ /**
+ * Returns the ISO-8601 week number of the given date.
+ *
+ * @param date
+ * The date for which the week number should be resolved
+ * @return The ISO-8601 week number for {@literal date}
+ */
+ public static int getISOWeekNumber(Date date) {
+ final long MILLISECONDS_PER_DAY = 24 * 3600 * 1000;
+ int dayOfWeek = date.getDay(); // 0 == sunday
+
+ // ISO 8601 use weeks that start on monday so we use
+ // mon=1,tue=2,...sun=7;
+ if (dayOfWeek == 0) {
+ dayOfWeek = 7;
+ }
+ // Find nearest thursday (defines the week in ISO 8601). The week number
+ // for the nearest thursday is the same as for the target date.
+ int nearestThursdayDiff = 4 - dayOfWeek; // 4 is thursday
+ Date nearestThursday = new Date(date.getTime() + nearestThursdayDiff
+ * MILLISECONDS_PER_DAY);
+
+ Date firstOfJanuary = new Date(nearestThursday.getYear(), 0, 1);
+ long timeDiff = nearestThursday.getTime() - firstOfJanuary.getTime();
+ int daysSinceFirstOfJanuary = (int) (timeDiff / MILLISECONDS_PER_DAY);
+
+ int weekNumber = (daysSinceFirstOfJanuary) / 7 + 1;
+
+ return weekNumber;
+ }
+
+ /**
+ * Check if format contains the month name. If it does we manually convert
+ * it to the month name since DateTimeFormat.format always uses the current
+ * locale and will replace the month name wrong if current locale is
+ * different from the locale set for the DateField.
+ *
+ * MMMM is converted into long month name, MMM is converted into short month
+ * name. '' are added around the name to avoid that DateTimeFormat parses
+ * the month name as a pattern.
+ *
+ * @param date
+ * The date to convert
+ * @param formatStr
+ * The format string that might contain MMM or MMMM
+ * @param dateTimeService
+ * Reference to the Vaadin DateTimeService
+ * @return
+ */
+ public String formatDate(Date date, String formatStr) {
+ /*
+ * Format month names separately when locale for the DateTimeService is
+ * not the same as the browser locale
+ */
+ formatStr = formatMonthNames(date, formatStr);
+
+ // Format uses the browser locale
+ DateTimeFormat format = DateTimeFormat.getFormat(formatStr);
+
+ String result = format.format(date);
+
+ return result;
+ }
+
+ private String formatMonthNames(Date date, String formatStr) {
+ if (formatStr.contains("MMMM")) {
+ String monthName = getMonth(date.getMonth());
+
+ if (monthName != null) {
+ /*
+ * Replace 4 or more M:s with the quoted month name. Also
+ * concatenate generated string with any other string prepending
+ * or following the MMMM pattern, i.e. 'MMMM'ta ' becomes
+ * 'MONTHta ' and not 'MONTH''ta ', 'ab'MMMM becomes 'abMONTH',
+ * 'x'MMMM'y' becomes 'xMONTHy'.
+ */
+ formatStr = formatStr.replaceAll("'([M]{4,})'", monthName);
+ formatStr = formatStr.replaceAll("([M]{4,})'", "'" + monthName);
+ formatStr = formatStr.replaceAll("'([M]{4,})", monthName + "'");
+ formatStr = formatStr.replaceAll("[M]{4,}", "'" + monthName
+ + "'");
+ }
+ }
+
+ if (formatStr.contains("MMM")) {
+
+ String monthName = getShortMonth(date.getMonth());
+
+ if (monthName != null) {
+ /*
+ * Replace 3 or more M:s with the quoted month name. Also
+ * concatenate generated string with any other string prepending
+ * or following the MMM pattern, i.e. 'MMM'ta ' becomes 'MONTHta
+ * ' and not 'MONTH''ta ', 'ab'MMM becomes 'abMONTH', 'x'MMM'y'
+ * becomes 'xMONTHy'.
+ */
+ formatStr = formatStr.replaceAll("'([M]{3,})'", monthName);
+ formatStr = formatStr.replaceAll("([M]{3,})'", "'" + monthName);
+ formatStr = formatStr.replaceAll("'([M]{3,})", monthName + "'");
+ formatStr = formatStr.replaceAll("[M]{3,}", "'" + monthName
+ + "'");
+ }
+ }
+
+ return formatStr;
+ }
+
+ /**
+ * Replaces month names in the entered date with the name in the current
+ * browser locale.
+ *
+ * @param enteredDate
+ * Date string e.g. "5 May 2010"
+ * @param formatString
+ * Format string e.g. "d M yyyy"
+ * @return The date string where the month names have been replaced by the
+ * browser locale version
+ */
+ private String parseMonthName(String enteredDate, String formatString) {
+ LocaleInfo browserLocale = LocaleInfo.getCurrentLocale();
+ if (browserLocale.getLocaleName().equals(getLocale())) {
+ // No conversion needs to be done when locales match
+ return enteredDate;
+ }
+ String[] browserMonthNames = browserLocale.getDateTimeConstants()
+ .months();
+ String[] browserShortMonthNames = browserLocale.getDateTimeConstants()
+ .shortMonths();
+
+ if (formatString.contains("MMMM")) {
+ // Full month name
+ for (int i = 0; i < 12; i++) {
+ enteredDate = enteredDate.replaceAll(getMonth(i),
+ browserMonthNames[i]);
+ }
+ }
+ if (formatString.contains("MMM")) {
+ // Short month name
+ for (int i = 0; i < 12; i++) {
+ enteredDate = enteredDate.replaceAll(getShortMonth(i),
+ browserShortMonthNames[i]);
+ }
+ }
+
+ return enteredDate;
+ }
+
+ /**
+ * Parses the given date string using the given format string and the locale
+ * set in this DateTimeService instance.
+ *
+ * @param dateString
+ * Date string e.g. "1 February 2010"
+ * @param formatString
+ * Format string e.g. "d MMMM yyyy"
+ * @param lenient
+ * true to use lenient parsing, false to use strict parsing
+ * @return A Date object representing the dateString. Never returns null.
+ * @throws IllegalArgumentException
+ * if the parsing fails
+ *
+ */
+ public Date parseDate(String dateString, String formatString,
+ boolean lenient) throws IllegalArgumentException {
+ /* DateTimeFormat uses the browser's locale */
+ DateTimeFormat format = DateTimeFormat.getFormat(formatString);
+
+ /*
+ * Parse month names separately when locale for the DateTimeService is
+ * not the same as the browser locale
+ */
+ dateString = parseMonthName(dateString, formatString);
+
+ Date date;
+
+ if (lenient) {
+ date = format.parse(dateString);
+ } else {
+ date = format.parseStrict(dateString);
+ }
+
+ // Some version of Firefox sets the timestamp to 0 if parsing fails.
+ if (date != null && date.getTime() == 0) {
+ throw new IllegalArgumentException("Parsing of '" + dateString
+ + "' failed");
+ }
+
+ return date;
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java b/client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java
new file mode 100644
index 0000000000..296fbb22ff
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/DirectionalManagedLayout.java
@@ -0,0 +1,12 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.vaadin.terminal.gwt.client.ui.ManagedLayout;
+
+public interface DirectionalManagedLayout extends ManagedLayout {
+ public void layoutVertically();
+
+ public void layoutHorizontally();
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/EventHelper.java b/client/src/com/vaadin/terminal/gwt/client/EventHelper.java
new file mode 100644
index 0000000000..208768a0c1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/EventHelper.java
@@ -0,0 +1,97 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import static com.vaadin.shared.EventId.BLUR;
+import static com.vaadin.shared.EventId.FOCUS;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.DomEvent.Type;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+
+/**
+ * Helper class for attaching/detaching handlers for Vaadin client side
+ * components, based on identifiers in UIDL. Helpers expect Paintables to be
+ * both listeners and sources for events. This helper cannot be used for more
+ * complex widgets.
+ * <p>
+ * Possible current registration is given as parameter. The returned
+ * registration (possibly the same as given, should be store for next update.
+ * <p>
+ * Pseudocode what helpers do:
+ *
+ * <pre>
+ *
+ * if paintable has event listener in UIDL
+ * if registration is null
+ * register paintable as as handler for event
+ * return the registration
+ * else
+ * if registration is not null
+ * remove the handler from paintable
+ * return null
+ *
+ *
+ * </pre>
+ *
+ */
+public class EventHelper {
+
+ /**
+ * Adds or removes a focus handler depending on if the connector has focus
+ * listeners on the server side or not.
+ *
+ * @param connector
+ * The connector to update. Must implement focusHandler.
+ * @param handlerRegistration
+ * The old registration reference or null no handler has been
+ * registered previously
+ * @return a new registration handler that can be used to unregister the
+ * handler later
+ */
+ public static <T extends ComponentConnector & FocusHandler> HandlerRegistration updateFocusHandler(
+ T connector, HandlerRegistration handlerRegistration) {
+ return updateHandler(connector, FOCUS, handlerRegistration,
+ FocusEvent.getType());
+ }
+
+ /**
+ * Adds or removes a blur handler depending on if the connector has blur
+ * listeners on the server side or not.
+ *
+ * @param connector
+ * The connector to update. Must implement BlurHandler.
+ * @param handlerRegistration
+ * The old registration reference or null no handler has been
+ * registered previously
+ * @return a new registration handler that can be used to unregister the
+ * handler later
+ */
+ public static <T extends ComponentConnector & BlurHandler> HandlerRegistration updateBlurHandler(
+ T connector, HandlerRegistration handlerRegistration) {
+ return updateHandler(connector, BLUR, handlerRegistration,
+ BlurEvent.getType());
+ }
+
+ private static <H extends EventHandler> HandlerRegistration updateHandler(
+ ComponentConnector connector, String eventIdentifier,
+ HandlerRegistration handlerRegistration, Type<H> type) {
+ if (connector.hasEventListener(eventIdentifier)) {
+ if (handlerRegistration == null) {
+ handlerRegistration = connector.getWidget().addDomHandler(
+ (H) connector, type);
+ }
+ } else if (handlerRegistration != null) {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+ return handlerRegistration;
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/FastStringSet.java b/client/src/com/vaadin/terminal/gwt/client/FastStringSet.java
new file mode 100644
index 0000000000..05ed8addc8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/FastStringSet.java
@@ -0,0 +1,60 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+public final class FastStringSet extends JavaScriptObject {
+ protected FastStringSet() {
+ // JSO constructor
+ }
+
+ public native boolean contains(String string)
+ /*-{
+ return this.hasOwnProperty(string);
+ }-*/;
+
+ public native void add(String string)
+ /*-{
+ this[string] = true;
+ }-*/;
+
+ public native void addAll(JsArrayString array)
+ /*-{
+ for(var i = 0; i < array.length; i++) {
+ this[array[i]] = true;
+ }
+ }-*/;
+
+ public native JsArrayString dump()
+ /*-{
+ var array = [];
+ for(var string in this) {
+ if (this.hasOwnProperty(string)) {
+ array.push(string);
+ }
+ }
+ return array;
+ }-*/;
+
+ public native void remove(String string)
+ /*-{
+ delete this[string];
+ }-*/;
+
+ public native boolean isEmpty()
+ /*-{
+ for(var string in this) {
+ if (this.hasOwnProperty(string)) {
+ return false;
+ }
+ }
+ return true;
+ }-*/;
+
+ public static FastStringSet create() {
+ return JavaScriptObject.createObject().cast();
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/Focusable.java b/client/src/com/vaadin/terminal/gwt/client/Focusable.java
new file mode 100644
index 0000000000..e8916bce40
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/Focusable.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+/**
+ * GWT's HasFocus is way too overkill for just receiving focus in simple
+ * components. Vaadin 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/client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java b/client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java
new file mode 100644
index 0000000000..47dd2df5b2
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/JavaScriptConnectorHelper.java
@@ -0,0 +1,372 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.user.client.Element;
+import com.vaadin.shared.JavaScriptConnectorState;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler;
+
+public class JavaScriptConnectorHelper {
+
+ private final ServerConnector connector;
+ private final JavaScriptObject nativeState = JavaScriptObject
+ .createObject();
+ private final JavaScriptObject rpcMap = JavaScriptObject.createObject();
+
+ private final Map<String, JavaScriptObject> rpcObjects = new HashMap<String, JavaScriptObject>();
+ private final Map<String, Set<String>> rpcMethods = new HashMap<String, Set<String>>();
+
+ private JavaScriptObject connectorWrapper;
+ private int tag;
+
+ private boolean inited = false;
+
+ public JavaScriptConnectorHelper(ServerConnector connector) {
+ this.connector = connector;
+
+ // Wildcard rpc object
+ rpcObjects.put("", JavaScriptObject.createObject());
+ }
+
+ public void init() {
+ connector.addStateChangeHandler(new StateChangeHandler() {
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ JavaScriptObject wrapper = getConnectorWrapper();
+ JavaScriptConnectorState state = getConnectorState();
+
+ for (String callback : state.getCallbackNames()) {
+ ensureCallback(JavaScriptConnectorHelper.this, wrapper,
+ callback);
+ }
+
+ for (Entry<String, Set<String>> entry : state
+ .getRpcInterfaces().entrySet()) {
+ String rpcName = entry.getKey();
+ String jsName = getJsInterfaceName(rpcName);
+ if (!rpcObjects.containsKey(jsName)) {
+ Set<String> methods = entry.getValue();
+ rpcObjects.put(jsName,
+ createRpcObject(rpcName, methods));
+
+ // Init all methods for wildcard rpc
+ for (String method : methods) {
+ JavaScriptObject wildcardRpcObject = rpcObjects
+ .get("");
+ Set<String> interfaces = rpcMethods.get(method);
+ if (interfaces == null) {
+ interfaces = new HashSet<String>();
+ rpcMethods.put(method, interfaces);
+ attachRpcMethod(wildcardRpcObject, null, method);
+ }
+ interfaces.add(rpcName);
+ }
+ }
+ }
+
+ // Init after setting up callbacks & rpc
+ if (!inited) {
+ initJavaScript();
+ inited = true;
+ }
+
+ fireNativeStateChange(wrapper);
+ }
+ });
+ }
+
+ private static String getJsInterfaceName(String rpcName) {
+ return rpcName.replace('$', '.');
+ }
+
+ protected JavaScriptObject createRpcObject(String iface, Set<String> methods) {
+ JavaScriptObject object = JavaScriptObject.createObject();
+
+ for (String method : methods) {
+ attachRpcMethod(object, iface, method);
+ }
+
+ return object;
+ }
+
+ private boolean initJavaScript() {
+ ApplicationConfiguration conf = connector.getConnection()
+ .getConfiguration();
+ ArrayList<String> attemptedNames = new ArrayList<String>();
+ Integer tag = Integer.valueOf(this.tag);
+ while (tag != null) {
+ String serverSideClassName = conf.getServerSideClassNameForTag(tag);
+ String initFunctionName = serverSideClassName
+ .replaceAll("\\.", "_");
+ if (tryInitJs(initFunctionName, getConnectorWrapper())) {
+ VConsole.log("JavaScript connector initialized using "
+ + initFunctionName);
+ return true;
+ } else {
+ VConsole.log("No JavaScript function " + initFunctionName
+ + " found");
+ attemptedNames.add(initFunctionName);
+ tag = conf.getParentTag(tag.intValue());
+ }
+ }
+ VConsole.log("No JavaScript init for connector not found");
+ showInitProblem(attemptedNames);
+ return false;
+ }
+
+ protected void showInitProblem(ArrayList<String> attemptedNames) {
+ // Default does nothing
+ }
+
+ private static native boolean tryInitJs(String initFunctionName,
+ JavaScriptObject connectorWrapper)
+ /*-{
+ if (typeof $wnd[initFunctionName] == 'function') {
+ $wnd[initFunctionName].apply(connectorWrapper);
+ return true;
+ } else {
+ return false;
+ }
+ }-*/;
+
+ private JavaScriptObject getConnectorWrapper() {
+ if (connectorWrapper == null) {
+ connectorWrapper = createConnectorWrapper(this,
+ connector.getConnection(), nativeState, rpcMap,
+ connector.getConnectorId(), rpcObjects);
+ }
+
+ return connectorWrapper;
+ }
+
+ private static native void fireNativeStateChange(
+ JavaScriptObject connectorWrapper)
+ /*-{
+ if (typeof connectorWrapper.onStateChange == 'function') {
+ connectorWrapper.onStateChange();
+ }
+ }-*/;
+
+ private static native JavaScriptObject createConnectorWrapper(
+ JavaScriptConnectorHelper h, ApplicationConnection c,
+ JavaScriptObject nativeState, JavaScriptObject registeredRpc,
+ String connectorId, Map<String, JavaScriptObject> rpcObjects)
+ /*-{
+ return {
+ 'getConnectorId': function() {
+ return connectorId;
+ },
+ 'getParentId': $entry(function(connectorId) {
+ return h.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::getParentId(Ljava/lang/String;)(connectorId);
+ }),
+ 'getState': function() {
+ return nativeState;
+ },
+ 'getRpcProxy': $entry(function(iface) {
+ if (!iface) {
+ iface = '';
+ }
+ return rpcObjects.@java.util.Map::get(Ljava/lang/Object;)(iface);
+ }),
+ 'getElement': $entry(function(connectorId) {
+ return h.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::getWidgetElement(Ljava/lang/String;)(connectorId);
+ }),
+ 'registerRpc': function(iface, rpcHandler) {
+ //registerRpc(handler) -> registerRpc('', handler);
+ if (!rpcHandler) {
+ rpcHandler = iface;
+ iface = '';
+ }
+ if (!registeredRpc[iface]) {
+ registeredRpc[iface] = [];
+ }
+ registeredRpc[iface].push(rpcHandler);
+ },
+ 'translateVaadinUri': $entry(function(uri) {
+ return c.@com.vaadin.terminal.gwt.client.ApplicationConnection::translateVaadinUri(Ljava/lang/String;)(uri);
+ }),
+ };
+ }-*/;
+
+ private native void attachRpcMethod(JavaScriptObject rpc, String iface,
+ String method)
+ /*-{
+ var self = this;
+ rpc[method] = $entry(function() {
+ self.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::fireRpc(Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(iface, method, arguments);
+ });
+ }-*/;
+
+ private String getParentId(String connectorId) {
+ ServerConnector target = getConnector(connectorId);
+ if (target == null) {
+ return null;
+ }
+ ServerConnector parent = target.getParent();
+ if (parent == null) {
+ return null;
+ } else {
+ return parent.getConnectorId();
+ }
+ }
+
+ private Element getWidgetElement(String connectorId) {
+ ServerConnector target = getConnector(connectorId);
+ if (target instanceof ComponentConnector) {
+ return ((ComponentConnector) target).getWidget().getElement();
+ } else {
+ return null;
+ }
+ }
+
+ private ServerConnector getConnector(String connectorId) {
+ if (connectorId == null || connectorId.length() == 0) {
+ return connector;
+ }
+
+ return ConnectorMap.get(connector.getConnection()).getConnector(
+ connectorId);
+ }
+
+ private void fireRpc(String iface, String method,
+ JsArray<JavaScriptObject> arguments) {
+ if (iface == null) {
+ iface = findWildcardInterface(method);
+ }
+
+ JSONArray argumentsArray = new JSONArray(arguments);
+ Object[] parameters = new Object[arguments.length()];
+ for (int i = 0; i < parameters.length; i++) {
+ parameters[i] = argumentsArray.get(i);
+ }
+ connector.getConnection().addMethodInvocationToQueue(
+ new MethodInvocation(connector.getConnectorId(), iface, method,
+ parameters), true);
+ }
+
+ private String findWildcardInterface(String method) {
+ Set<String> interfaces = rpcMethods.get(method);
+ if (interfaces.size() == 1) {
+ return interfaces.iterator().next();
+ } else {
+ // TODO Resolve conflicts using argument count and types
+ String interfaceList = "";
+ for (String iface : interfaces) {
+ if (interfaceList.length() != 0) {
+ interfaceList += ", ";
+ }
+ interfaceList += getJsInterfaceName(iface);
+ }
+
+ throw new IllegalStateException(
+ "Can not call method "
+ + method
+ + " for wildcard rpc proxy because the function is defined for multiple rpc interfaces: "
+ + interfaceList
+ + ". Retrieve a rpc proxy for a specific interface using getRpcProxy(interfaceName) to use the function.");
+ }
+ }
+
+ private void fireCallback(String name, JsArray<JavaScriptObject> arguments) {
+ MethodInvocation invocation = new MethodInvocation(
+ connector.getConnectorId(),
+ "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call",
+ new Object[] { name, new JSONArray(arguments) });
+ connector.getConnection().addMethodInvocationToQueue(invocation, true);
+ }
+
+ public void setNativeState(JavaScriptObject state) {
+ updateNativeState(nativeState, state);
+ }
+
+ private static native void updateNativeState(JavaScriptObject state,
+ JavaScriptObject input)
+ /*-{
+ // Copy all fields to existing state object
+ for(var key in state) {
+ if (state.hasOwnProperty(key)) {
+ delete state[key];
+ }
+ }
+
+ for(var key in input) {
+ if (input.hasOwnProperty(key)) {
+ state[key] = input[key];
+ }
+ }
+ }-*/;
+
+ public Object[] decodeRpcParameters(JSONArray parametersJson) {
+ return new Object[] { parametersJson.getJavaScriptObject() };
+ }
+
+ public void setTag(int tag) {
+ this.tag = tag;
+ }
+
+ public void invokeJsRpc(MethodInvocation invocation,
+ JSONArray parametersJson) {
+ String iface = invocation.getInterfaceName();
+ String method = invocation.getMethodName();
+ if ("com.vaadin.ui.JavaScript$JavaScriptCallbackRpc".equals(iface)
+ && "call".equals(method)) {
+ String callbackName = parametersJson.get(0).isString()
+ .stringValue();
+ JavaScriptObject arguments = parametersJson.get(1).isArray()
+ .getJavaScriptObject();
+ invokeCallback(getConnectorWrapper(), callbackName, arguments);
+ } else {
+ JavaScriptObject arguments = parametersJson.getJavaScriptObject();
+ invokeJsRpc(rpcMap, iface, method, arguments);
+ // Also invoke wildcard interface
+ invokeJsRpc(rpcMap, "", method, arguments);
+ }
+ }
+
+ private static native void invokeCallback(JavaScriptObject connector,
+ String name, JavaScriptObject arguments)
+ /*-{
+ connector[name].apply(connector, arguments);
+ }-*/;
+
+ private static native void invokeJsRpc(JavaScriptObject rpcMap,
+ String interfaceName, String methodName, JavaScriptObject parameters)
+ /*-{
+ var targets = rpcMap[interfaceName];
+ if (!targets) {
+ return;
+ }
+ for(var i = 0; i < targets.length; i++) {
+ var target = targets[i];
+ target[methodName].apply(target, parameters);
+ }
+ }-*/;
+
+ private static native void ensureCallback(JavaScriptConnectorHelper h,
+ JavaScriptObject connector, String name)
+ /*-{
+ connector[name] = $entry(function() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ h.@com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper::fireCallback(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args);
+ });
+ }-*/;
+
+ private JavaScriptConnectorState getConnectorState() {
+ return (JavaScriptConnectorState) connector.getState();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java b/client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java
new file mode 100644
index 0000000000..a2170b9ab9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/JavaScriptExtension.java
@@ -0,0 +1,34 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.AbstractJavaScriptExtension;
+import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector;
+
+@Connect(AbstractJavaScriptExtension.class)
+public final class JavaScriptExtension extends AbstractExtensionConnector
+ implements HasJavaScriptConnectorHelper {
+ private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper(
+ this);
+
+ @Override
+ protected void init() {
+ super.init();
+ helper.init();
+ }
+
+ @Override
+ public JavaScriptConnectorHelper getJavascriptConnectorHelper() {
+ return helper;
+ }
+
+ @Override
+ public JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/LayoutManager.java b/client/src/com/vaadin/terminal/gwt/client/LayoutManager.java
new file mode 100644
index 0000000000..74586a6def
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/LayoutManager.java
@@ -0,0 +1,1215 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.user.client.Timer;
+import com.vaadin.terminal.gwt.client.MeasuredSize.MeasureResult;
+import com.vaadin.terminal.gwt.client.ui.ManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeEvent;
+import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeListener;
+import com.vaadin.terminal.gwt.client.ui.layout.LayoutDependencyTree;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+
+public class LayoutManager {
+ private static final String LOOP_ABORT_MESSAGE = "Aborting layout after 100 passes. This would probably be an infinite loop.";
+
+ private static final boolean debugLogging = false;
+
+ private ApplicationConnection connection;
+ private final Set<Element> measuredNonConnectorElements = new HashSet<Element>();
+ private final MeasuredSize nullSize = new MeasuredSize();
+
+ private LayoutDependencyTree currentDependencyTree;
+
+ private final Collection<ManagedLayout> needsHorizontalLayout = new HashSet<ManagedLayout>();
+ private final Collection<ManagedLayout> needsVerticalLayout = new HashSet<ManagedLayout>();
+
+ private final Collection<ComponentConnector> needsMeasure = new HashSet<ComponentConnector>();
+
+ private Collection<ComponentConnector> pendingOverflowFixes = new HashSet<ComponentConnector>();
+
+ private final Map<Element, Collection<ElementResizeListener>> elementResizeListeners = new HashMap<Element, Collection<ElementResizeListener>>();
+ private final Set<Element> listenersToFire = new HashSet<Element>();
+
+ private boolean layoutPending = false;
+ private Timer layoutTimer = new Timer() {
+ @Override
+ public void run() {
+ cancel();
+ layoutNow();
+ }
+ };
+ private boolean everythingNeedsMeasure = false;
+
+ public void setConnection(ApplicationConnection connection) {
+ if (this.connection != null) {
+ throw new RuntimeException(
+ "LayoutManager connection can never be changed");
+ }
+ this.connection = connection;
+ }
+
+ /**
+ * Gets the layout manager associated with the given
+ * {@link ApplicationConnection}.
+ *
+ * @param connection
+ * the application connection to get a layout manager for
+ * @return the layout manager associated with the provided application
+ * connection
+ */
+ public static LayoutManager get(ApplicationConnection connection) {
+ return connection.getLayoutManager();
+ }
+
+ /**
+ * Registers that a ManagedLayout is depending on the size of an Element.
+ * This causes this layout manager to measure the element in the beginning
+ * of every layout phase and call the appropriate layout method of the
+ * managed layout if the size of the element has changed.
+ *
+ * @param owner
+ * the ManagedLayout that depends on an element
+ * @param element
+ * the Element that should be measured
+ */
+ public void registerDependency(ManagedLayout owner, Element element) {
+ MeasuredSize measuredSize = ensureMeasured(element);
+ setNeedsLayout(owner);
+ measuredSize.addDependent(owner.getConnectorId());
+ }
+
+ private MeasuredSize ensureMeasured(Element element) {
+ MeasuredSize measuredSize = getMeasuredSize(element, null);
+ if (measuredSize == null) {
+ measuredSize = new MeasuredSize();
+
+ if (ConnectorMap.get(connection).getConnector(element) == null) {
+ measuredNonConnectorElements.add(element);
+ }
+ setMeasuredSize(element, measuredSize);
+ }
+ return measuredSize;
+ }
+
+ private boolean needsMeasure(Element e) {
+ if (connection.getConnectorMap().getConnectorId(e) != null) {
+ return true;
+ } else if (elementResizeListeners.containsKey(e)) {
+ return true;
+ } else if (getMeasuredSize(e, nullSize).hasDependents()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Assigns a measured size to an element. Method defined as protected to
+ * allow separate implementation for IE8.
+ *
+ * @param element
+ * the dom element to attach the measured size to
+ * @param measuredSize
+ * the measured size to attach to the element. If
+ * <code>null</code>, any previous measured size is removed.
+ */
+ protected native void setMeasuredSize(Element element,
+ MeasuredSize measuredSize)
+ /*-{
+ if (measuredSize) {
+ element.vMeasuredSize = measuredSize;
+ } else {
+ delete element.vMeasuredSize;
+ }
+ }-*/;
+
+ /**
+ * Gets the measured size for an element. Method defined as protected to
+ * allow separate implementation for IE8.
+ *
+ * @param element
+ * The element to get measured size for
+ * @param defaultSize
+ * The size to return if no measured size could be found
+ * @return The measured size for the element or {@literal defaultSize}
+ */
+ protected native MeasuredSize getMeasuredSize(Element element,
+ MeasuredSize defaultSize)
+ /*-{
+ return element.vMeasuredSize || defaultSize;
+ }-*/;
+
+ private final MeasuredSize getMeasuredSize(ComponentConnector connector) {
+ Element element = connector.getWidget().getElement();
+ MeasuredSize measuredSize = getMeasuredSize(element, null);
+ if (measuredSize == null) {
+ measuredSize = new MeasuredSize();
+ setMeasuredSize(element, measuredSize);
+ }
+ return measuredSize;
+ }
+
+ /**
+ * Registers that a ManagedLayout is no longer depending on the size of an
+ * Element.
+ *
+ * @see #registerDependency(ManagedLayout, Element)
+ *
+ * @param owner
+ * the ManagedLayout no longer depends on an element
+ * @param element
+ * the Element that that no longer needs to be measured
+ */
+ public void unregisterDependency(ManagedLayout owner, Element element) {
+ MeasuredSize measuredSize = getMeasuredSize(element, null);
+ if (measuredSize == null) {
+ return;
+ }
+ measuredSize.removeDependent(owner.getConnectorId());
+ stopMeasuringIfUnecessary(element);
+ }
+
+ public boolean isLayoutRunning() {
+ return currentDependencyTree != null;
+ }
+
+ private void countLayout(Map<ManagedLayout, Integer> layoutCounts,
+ ManagedLayout layout) {
+ Integer count = layoutCounts.get(layout);
+ if (count == null) {
+ count = Integer.valueOf(0);
+ } else {
+ count = Integer.valueOf(count.intValue() + 1);
+ }
+ layoutCounts.put(layout, count);
+ if (count.intValue() > 2) {
+ VConsole.error(Util.getConnectorString(layout)
+ + " has been layouted " + count.intValue() + " times");
+ }
+ }
+
+ private void layoutLater() {
+ if (!layoutPending) {
+ layoutPending = true;
+ layoutTimer.schedule(100);
+ }
+ }
+
+ public void layoutNow() {
+ if (isLayoutRunning()) {
+ throw new IllegalStateException(
+ "Can't start a new layout phase before the previous layout phase ends.");
+ }
+ layoutPending = false;
+ try {
+ currentDependencyTree = new LayoutDependencyTree();
+ doLayout();
+ } finally {
+ currentDependencyTree = null;
+ }
+ }
+
+ private void doLayout() {
+ VConsole.log("Starting layout phase");
+
+ Map<ManagedLayout, Integer> layoutCounts = new HashMap<ManagedLayout, Integer>();
+
+ int passes = 0;
+ Duration totalDuration = new Duration();
+
+ for (ManagedLayout layout : needsHorizontalLayout) {
+ currentDependencyTree.setNeedsHorizontalLayout(layout, true);
+ }
+ for (ManagedLayout layout : needsVerticalLayout) {
+ currentDependencyTree.setNeedsVerticalLayout(layout, true);
+ }
+ needsHorizontalLayout.clear();
+ needsVerticalLayout.clear();
+
+ for (ComponentConnector connector : needsMeasure) {
+ currentDependencyTree.setNeedsMeasure(connector, true);
+ }
+ needsMeasure.clear();
+
+ measureNonConnectors();
+
+ VConsole.log("Layout init in " + totalDuration.elapsedMillis() + " ms");
+
+ while (true) {
+ Duration passDuration = new Duration();
+ passes++;
+
+ int measuredConnectorCount = measureConnectors(
+ currentDependencyTree, everythingNeedsMeasure);
+ everythingNeedsMeasure = false;
+ if (measuredConnectorCount == 0) {
+ VConsole.log("No more changes in pass " + passes);
+ break;
+ }
+
+ int measureTime = passDuration.elapsedMillis();
+ VConsole.log(" Measured " + measuredConnectorCount
+ + " elements in " + measureTime + " ms");
+
+ if (!listenersToFire.isEmpty()) {
+ for (Element element : listenersToFire) {
+ Collection<ElementResizeListener> listeners = elementResizeListeners
+ .get(element);
+ ElementResizeListener[] array = listeners
+ .toArray(new ElementResizeListener[listeners.size()]);
+ ElementResizeEvent event = new ElementResizeEvent(this,
+ element);
+ for (ElementResizeListener listener : array) {
+ try {
+ listener.onElementResize(event);
+ } catch (RuntimeException e) {
+ VConsole.error(e);
+ }
+ }
+ }
+ int measureListenerTime = passDuration.elapsedMillis();
+ VConsole.log(" Fired resize listeners for "
+ + listenersToFire.size() + " elements in "
+ + (measureListenerTime - measureTime) + " ms");
+ measureTime = measuredConnectorCount;
+ listenersToFire.clear();
+ }
+
+ FastStringSet updatedSet = FastStringSet.create();
+
+ while (currentDependencyTree.hasHorizontalConnectorToLayout()
+ || currentDependencyTree.hasVerticaConnectorToLayout()) {
+ for (ManagedLayout layout : currentDependencyTree
+ .getHorizontalLayoutTargets()) {
+ if (layout instanceof DirectionalManagedLayout) {
+ currentDependencyTree
+ .markAsHorizontallyLayouted(layout);
+ DirectionalManagedLayout cl = (DirectionalManagedLayout) layout;
+ try {
+ cl.layoutHorizontally();
+ } catch (RuntimeException e) {
+ VConsole.log(e);
+ }
+ countLayout(layoutCounts, cl);
+ } else {
+ currentDependencyTree
+ .markAsHorizontallyLayouted(layout);
+ currentDependencyTree.markAsVerticallyLayouted(layout);
+ SimpleManagedLayout rr = (SimpleManagedLayout) layout;
+ try {
+ rr.layout();
+ } catch (RuntimeException e) {
+ VConsole.log(e);
+ }
+ countLayout(layoutCounts, rr);
+ }
+ if (debugLogging) {
+ updatedSet.add(layout.getConnectorId());
+ }
+ }
+
+ for (ManagedLayout layout : currentDependencyTree
+ .getVerticalLayoutTargets()) {
+ if (layout instanceof DirectionalManagedLayout) {
+ currentDependencyTree.markAsVerticallyLayouted(layout);
+ DirectionalManagedLayout cl = (DirectionalManagedLayout) layout;
+ try {
+ cl.layoutVertically();
+ } catch (RuntimeException e) {
+ VConsole.log(e);
+ }
+ countLayout(layoutCounts, cl);
+ } else {
+ currentDependencyTree
+ .markAsHorizontallyLayouted(layout);
+ currentDependencyTree.markAsVerticallyLayouted(layout);
+ SimpleManagedLayout rr = (SimpleManagedLayout) layout;
+ try {
+ rr.layout();
+ } catch (RuntimeException e) {
+ VConsole.log(e);
+ }
+ countLayout(layoutCounts, rr);
+ }
+ if (debugLogging) {
+ updatedSet.add(layout.getConnectorId());
+ }
+ }
+ }
+
+ if (debugLogging) {
+ JsArrayString changedCids = updatedSet.dump();
+
+ StringBuilder b = new StringBuilder(" ");
+ b.append(changedCids.length());
+ b.append(" requestLayout invocations in ");
+ b.append(passDuration.elapsedMillis() - measureTime);
+ b.append(" ms");
+ if (changedCids.length() < 30) {
+ for (int i = 0; i < changedCids.length(); i++) {
+ if (i != 0) {
+ b.append(", ");
+ } else {
+ b.append(": ");
+ }
+ String connectorString = changedCids.get(i);
+ if (changedCids.length() < 10) {
+ ServerConnector connector = ConnectorMap.get(
+ connection).getConnector(connectorString);
+ connectorString = Util
+ .getConnectorString(connector);
+ }
+ b.append(connectorString);
+ }
+ }
+ VConsole.log(b.toString());
+ }
+
+ VConsole.log("Pass " + passes + " completed in "
+ + passDuration.elapsedMillis() + " ms");
+
+ if (passes > 100) {
+ VConsole.log(LOOP_ABORT_MESSAGE);
+ VNotification.createNotification(VNotification.DELAY_FOREVER)
+ .show(LOOP_ABORT_MESSAGE, VNotification.CENTERED,
+ "error");
+ break;
+ }
+ }
+
+ int postLayoutStart = totalDuration.elapsedMillis();
+ for (ComponentConnector connector : connection.getConnectorMap()
+ .getComponentConnectors()) {
+ if (connector instanceof PostLayoutListener) {
+ ((PostLayoutListener) connector).postLayout();
+ }
+ }
+ int postLayoutDone = (totalDuration.elapsedMillis() - postLayoutStart);
+ VConsole.log("Invoke post layout listeners in " + postLayoutDone
+ + " ms");
+
+ cleanMeasuredSizes();
+ int cleaningDone = (totalDuration.elapsedMillis() - postLayoutDone);
+ VConsole.log("Cleaned old measured sizes in " + cleaningDone + "ms");
+
+ VConsole.log("Total layout phase time: "
+ + totalDuration.elapsedMillis() + "ms");
+ }
+
+ private void logConnectorStatus(int connectorId) {
+ currentDependencyTree
+ .logDependencyStatus((ComponentConnector) ConnectorMap.get(
+ connection).getConnector(Integer.toString(connectorId)));
+ }
+
+ private int measureConnectors(LayoutDependencyTree layoutDependencyTree,
+ boolean measureAll) {
+ if (!pendingOverflowFixes.isEmpty()) {
+ Duration duration = new Duration();
+
+ HashMap<Element, String> originalOverflows = new HashMap<Element, String>();
+
+ HashSet<ComponentConnector> delayedOverflowFixes = new HashSet<ComponentConnector>();
+
+ // First set overflow to hidden (and save previous value so it can
+ // be restored later)
+ for (ComponentConnector componentConnector : pendingOverflowFixes) {
+ // Delay the overflow fix if the involved connectors might still
+ // change
+ boolean connectorChangesExpected = !currentDependencyTree
+ .noMoreChangesExpected(componentConnector);
+ boolean parentChangesExcpected = componentConnector.getParent() instanceof ComponentConnector
+ && !currentDependencyTree
+ .noMoreChangesExpected((ComponentConnector) componentConnector
+ .getParent());
+ if (connectorChangesExpected || parentChangesExcpected) {
+ delayedOverflowFixes.add(componentConnector);
+ continue;
+ }
+
+ if (debugLogging) {
+ VConsole.log("Doing overflow fix for "
+ + Util.getConnectorString(componentConnector)
+ + " in "
+ + Util.getConnectorString(componentConnector
+ .getParent()));
+ }
+
+ Element parentElement = componentConnector.getWidget()
+ .getElement().getParentElement();
+ Style style = parentElement.getStyle();
+ String originalOverflow = style.getOverflow();
+
+ if (originalOverflow != null
+ && !originalOverflows.containsKey(parentElement)) {
+ // Store original value for restore, but only the first time
+ // the value is changed
+ originalOverflows.put(parentElement, originalOverflow);
+ }
+
+ style.setOverflow(Overflow.HIDDEN);
+ }
+
+ pendingOverflowFixes.removeAll(delayedOverflowFixes);
+
+ // Then ensure all scrolling elements are reflowed by measuring
+ for (ComponentConnector componentConnector : pendingOverflowFixes) {
+ componentConnector.getWidget().getElement().getParentElement()
+ .getOffsetHeight();
+ }
+
+ // Finally restore old overflow value and update bookkeeping
+ for (ComponentConnector componentConnector : pendingOverflowFixes) {
+ Element parentElement = componentConnector.getWidget()
+ .getElement().getParentElement();
+ parentElement.getStyle().setProperty("overflow",
+ originalOverflows.get(parentElement));
+
+ layoutDependencyTree.setNeedsMeasure(componentConnector, true);
+ }
+ if (!pendingOverflowFixes.isEmpty()) {
+ VConsole.log("Did overflow fix for "
+ + pendingOverflowFixes.size() + " elements in "
+ + duration.elapsedMillis() + " ms");
+ }
+ pendingOverflowFixes = delayedOverflowFixes;
+ }
+
+ int measureCount = 0;
+ if (measureAll) {
+ ComponentConnector[] connectors = ConnectorMap.get(connection)
+ .getComponentConnectors();
+ for (ComponentConnector connector : connectors) {
+ measureConnector(connector);
+ }
+ for (ComponentConnector connector : connectors) {
+ layoutDependencyTree.setNeedsMeasure(connector, false);
+ }
+ measureCount += connectors.length;
+ }
+
+ while (layoutDependencyTree.hasConnectorsToMeasure()) {
+ Collection<ComponentConnector> measureTargets = layoutDependencyTree
+ .getMeasureTargets();
+ for (ComponentConnector connector : measureTargets) {
+ measureConnector(connector);
+ measureCount++;
+ }
+ for (ComponentConnector connector : measureTargets) {
+ layoutDependencyTree.setNeedsMeasure(connector, false);
+ }
+ }
+ return measureCount;
+ }
+
+ private void measureConnector(ComponentConnector connector) {
+ Element element = connector.getWidget().getElement();
+ MeasuredSize measuredSize = getMeasuredSize(connector);
+ MeasureResult measureResult = measuredAndUpdate(element, measuredSize);
+
+ if (measureResult.isChanged()) {
+ onConnectorChange(connector, measureResult.isWidthChanged(),
+ measureResult.isHeightChanged());
+ }
+ }
+
+ private void onConnectorChange(ComponentConnector connector,
+ boolean widthChanged, boolean heightChanged) {
+ setNeedsOverflowFix(connector);
+ if (heightChanged) {
+ currentDependencyTree.markHeightAsChanged(connector);
+ }
+ if (widthChanged) {
+ currentDependencyTree.markWidthAsChanged(connector);
+ }
+ }
+
+ private void setNeedsOverflowFix(ComponentConnector connector) {
+ // IE9 doesn't need the original fix, but for some reason it needs this
+ if (BrowserInfo.get().requiresOverflowAutoFix()
+ || BrowserInfo.get().isIE9()) {
+ ComponentConnector scrollingBoundary = currentDependencyTree
+ .getScrollingBoundary(connector);
+ if (scrollingBoundary != null) {
+ pendingOverflowFixes.add(scrollingBoundary);
+ }
+ }
+ }
+
+ private void measureNonConnectors() {
+ for (Element element : measuredNonConnectorElements) {
+ measuredAndUpdate(element, getMeasuredSize(element, null));
+ }
+ VConsole.log("Measured " + measuredNonConnectorElements.size()
+ + " non connector elements");
+ }
+
+ private MeasureResult measuredAndUpdate(Element element,
+ MeasuredSize measuredSize) {
+ MeasureResult measureResult = measuredSize.measure(element);
+ if (measureResult.isChanged()) {
+ notifyListenersAndDepdendents(element,
+ measureResult.isWidthChanged(),
+ measureResult.isHeightChanged());
+ }
+ return measureResult;
+ }
+
+ private void notifyListenersAndDepdendents(Element element,
+ boolean widthChanged, boolean heightChanged) {
+ assert widthChanged || heightChanged;
+
+ MeasuredSize measuredSize = getMeasuredSize(element, nullSize);
+ JsArrayString dependents = measuredSize.getDependents();
+ for (int i = 0; i < dependents.length(); i++) {
+ String pid = dependents.get(i);
+ ManagedLayout dependent = (ManagedLayout) connection
+ .getConnectorMap().getConnector(pid);
+ if (dependent != null) {
+ if (heightChanged) {
+ currentDependencyTree.setNeedsVerticalLayout(dependent,
+ true);
+ }
+ if (widthChanged) {
+ currentDependencyTree.setNeedsHorizontalLayout(dependent,
+ true);
+ }
+ }
+ }
+ if (elementResizeListeners.containsKey(element)) {
+ listenersToFire.add(element);
+ }
+ }
+
+ private static boolean isManagedLayout(ComponentConnector connector) {
+ return connector instanceof ManagedLayout;
+ }
+
+ public void forceLayout() {
+ ConnectorMap connectorMap = connection.getConnectorMap();
+ ComponentConnector[] componentConnectors = connectorMap
+ .getComponentConnectors();
+ for (ComponentConnector connector : componentConnectors) {
+ if (connector instanceof ManagedLayout) {
+ setNeedsLayout((ManagedLayout) connector);
+ }
+ }
+ setEverythingNeedsMeasure();
+ layoutNow();
+ }
+
+ /**
+ * Marks that a ManagedLayout should be layouted in the next layout phase
+ * even if none of the elements managed by the layout have been resized.
+ *
+ * @param layout
+ * the managed layout that should be layouted
+ */
+ public final void setNeedsLayout(ManagedLayout layout) {
+ setNeedsHorizontalLayout(layout);
+ setNeedsVerticalLayout(layout);
+ }
+
+ /**
+ * Marks that a ManagedLayout should be layouted horizontally in the next
+ * layout phase even if none of the elements managed by the layout have been
+ * resized horizontally.
+ *
+ * For SimpleManagedLayout which is always layouted in both directions, this
+ * has the same effect as {@link #setNeedsLayout(ManagedLayout)}.
+ *
+ * @param layout
+ * the managed layout that should be layouted
+ */
+ public final void setNeedsHorizontalLayout(ManagedLayout layout) {
+ needsHorizontalLayout.add(layout);
+ }
+
+ /**
+ * Marks that a ManagedLayout should be layouted vertically in the next
+ * layout phase even if none of the elements managed by the layout have been
+ * resized vertically.
+ *
+ * For SimpleManagedLayout which is always layouted in both directions, this
+ * has the same effect as {@link #setNeedsLayout(ManagedLayout)}.
+ *
+ * @param layout
+ * the managed layout that should be layouted
+ */
+ public final void setNeedsVerticalLayout(ManagedLayout layout) {
+ needsVerticalLayout.add(layout);
+ }
+
+ /**
+ * Gets the outer height (including margins, paddings and borders) of the
+ * given element, provided that it has been measured. These elements are
+ * guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * -1 is returned if the element has not been measured. If 0 is returned, it
+ * might indicate that the element is not attached to the DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured outer height (including margins, paddings and
+ * borders) of the element in pixels.
+ */
+ public final int getOuterHeight(Element element) {
+ return getMeasuredSize(element, nullSize).getOuterHeight();
+ }
+
+ /**
+ * Gets the outer width (including margins, paddings and borders) of the
+ * given element, provided that it has been measured. These elements are
+ * guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * -1 is returned if the element has not been measured. If 0 is returned, it
+ * might indicate that the element is not attached to the DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured outer width (including margins, paddings and
+ * borders) of the element in pixels.
+ */
+ public final int getOuterWidth(Element element) {
+ return getMeasuredSize(element, nullSize).getOuterWidth();
+ }
+
+ /**
+ * Gets the inner height (excluding margins, paddings and borders) of the
+ * given element, provided that it has been measured. These elements are
+ * guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * -1 is returned if the element has not been measured. If 0 is returned, it
+ * might indicate that the element is not attached to the DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured inner height (excluding margins, paddings and
+ * borders) of the element in pixels.
+ */
+ public final int getInnerHeight(Element element) {
+ return getMeasuredSize(element, nullSize).getInnerHeight();
+ }
+
+ /**
+ * Gets the inner width (excluding margins, paddings and borders) of the
+ * given element, provided that it has been measured. These elements are
+ * guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * -1 is returned if the element has not been measured. If 0 is returned, it
+ * might indicate that the element is not attached to the DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured inner width (excluding margins, paddings and
+ * borders) of the element in pixels.
+ */
+ public final int getInnerWidth(Element element) {
+ return getMeasuredSize(element, nullSize).getInnerWidth();
+ }
+
+ /**
+ * Gets the border height (top border + bottom border) of the given element,
+ * provided that it has been measured. These elements are guaranteed to be
+ * measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured border height (top border + bottom border) of the
+ * element in pixels.
+ */
+ public final int getBorderHeight(Element element) {
+ return getMeasuredSize(element, nullSize).getBorderHeight();
+ }
+
+ /**
+ * Gets the padding height (top padding + bottom padding) of the given
+ * element, provided that it has been measured. These elements are
+ * guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured padding height (top padding + bottom padding) of the
+ * element in pixels.
+ */
+ public int getPaddingHeight(Element element) {
+ return getMeasuredSize(element, nullSize).getPaddingHeight();
+ }
+
+ /**
+ * Gets the border width (left border + right border) of the given element,
+ * provided that it has been measured. These elements are guaranteed to be
+ * measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured border width (left border + right border) of the
+ * element in pixels.
+ */
+ public int getBorderWidth(Element element) {
+ return getMeasuredSize(element, nullSize).getBorderWidth();
+ }
+
+ /**
+ * Gets the padding width (left padding + right padding) of the given
+ * element, provided that it has been measured. These elements are
+ * guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured padding width (left padding + right padding) of the
+ * element in pixels.
+ */
+ public int getPaddingWidth(Element element) {
+ return getMeasuredSize(element, nullSize).getPaddingWidth();
+ }
+
+ /**
+ * Gets the top padding of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured top padding of the element in pixels.
+ */
+ public int getPaddingTop(Element element) {
+ return getMeasuredSize(element, nullSize).getPaddingTop();
+ }
+
+ /**
+ * Gets the left padding of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured left padding of the element in pixels.
+ */
+ public int getPaddingLeft(Element element) {
+ return getMeasuredSize(element, nullSize).getPaddingLeft();
+ }
+
+ /**
+ * Gets the bottom padding of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured bottom padding of the element in pixels.
+ */
+ public int getPaddingBottom(Element element) {
+ return getMeasuredSize(element, nullSize).getPaddingBottom();
+ }
+
+ /**
+ * Gets the right padding of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured right padding of the element in pixels.
+ */
+ public int getPaddingRight(Element element) {
+ return getMeasuredSize(element, nullSize).getPaddingRight();
+ }
+
+ /**
+ * Gets the top margin of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured top margin of the element in pixels.
+ */
+ public int getMarginTop(Element element) {
+ return getMeasuredSize(element, nullSize).getMarginTop();
+ }
+
+ /**
+ * Gets the right margin of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured right margin of the element in pixels.
+ */
+ public int getMarginRight(Element element) {
+ return getMeasuredSize(element, nullSize).getMarginRight();
+ }
+
+ /**
+ * Gets the bottom margin of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured bottom margin of the element in pixels.
+ */
+ public int getMarginBottom(Element element) {
+ return getMeasuredSize(element, nullSize).getMarginBottom();
+ }
+
+ /**
+ * Gets the left margin of the given element, provided that it has been
+ * measured. These elements are guaranteed to be measured:
+ * <ul>
+ * <li>ManagedLayotus and their child Connectors
+ * <li>Elements for which there is at least one ElementResizeListener
+ * <li>Elements for which at least one ManagedLayout has registered a
+ * dependency
+ * </ul>
+ *
+ * A negative number is returned if the element has not been measured. If 0
+ * is returned, it might indicate that the element is not attached to the
+ * DOM.
+ *
+ * @param element
+ * the element to get the measured size for
+ * @return the measured left margin of the element in pixels.
+ */
+ public int getMarginLeft(Element element) {
+ return getMeasuredSize(element, nullSize).getMarginLeft();
+ }
+
+ /**
+ * Registers the outer height (including margins, borders and paddings) of a
+ * component. This can be used as an optimization by ManagedLayouts; by
+ * informing the LayoutManager about what size a component will have, the
+ * layout propagation can continue directly without first measuring the
+ * potentially resized elements.
+ *
+ * @param component
+ * the component for which the size is reported
+ * @param outerHeight
+ * the new outer height (including margins, borders and paddings)
+ * of the component in pixels
+ */
+ public void reportOuterHeight(ComponentConnector component, int outerHeight) {
+ MeasuredSize measuredSize = getMeasuredSize(component);
+ if (isLayoutRunning()) {
+ boolean heightChanged = measuredSize.setOuterHeight(outerHeight);
+
+ if (heightChanged) {
+ onConnectorChange(component, false, true);
+ notifyListenersAndDepdendents(component.getWidget()
+ .getElement(), false, true);
+ }
+ currentDependencyTree.setNeedsVerticalMeasure(component, false);
+ } else if (measuredSize.getOuterHeight() != outerHeight) {
+ setNeedsMeasure(component);
+ }
+ }
+
+ /**
+ * Registers the height reserved for a relatively sized component. This can
+ * be used as an optimization by ManagedLayouts; by informing the
+ * LayoutManager about what size a component will have, the layout
+ * propagation can continue directly without first measuring the potentially
+ * resized elements.
+ *
+ * @param component
+ * the relatively sized component for which the size is reported
+ * @param assignedHeight
+ * the inner height of the relatively sized component's parent
+ * element in pixels
+ */
+ public void reportHeightAssignedToRelative(ComponentConnector component,
+ int assignedHeight) {
+ assert component.isRelativeHeight();
+
+ float percentSize = parsePercent(component.getState().getHeight());
+ int effectiveHeight = Math.round(assignedHeight * (percentSize / 100));
+
+ reportOuterHeight(component, effectiveHeight);
+ }
+
+ /**
+ * Registers the width reserved for a relatively sized component. This can
+ * be used as an optimization by ManagedLayouts; by informing the
+ * LayoutManager about what size a component will have, the layout
+ * propagation can continue directly without first measuring the potentially
+ * resized elements.
+ *
+ * @param component
+ * the relatively sized component for which the size is reported
+ * @param assignedWidth
+ * the inner width of the relatively sized component's parent
+ * element in pixels
+ */
+ public void reportWidthAssignedToRelative(ComponentConnector component,
+ int assignedWidth) {
+ assert component.isRelativeWidth();
+
+ float percentSize = parsePercent(component.getState().getWidth());
+ int effectiveWidth = Math.round(assignedWidth * (percentSize / 100));
+
+ reportOuterWidth(component, effectiveWidth);
+ }
+
+ private static float parsePercent(String size) {
+ return Float.parseFloat(size.substring(0, size.length() - 1));
+ }
+
+ /**
+ * Registers the outer width (including margins, borders and paddings) of a
+ * component. This can be used as an optimization by ManagedLayouts; by
+ * informing the LayoutManager about what size a component will have, the
+ * layout propagation can continue directly without first measuring the
+ * potentially resized elements.
+ *
+ * @param component
+ * the component for which the size is reported
+ * @param outerWidth
+ * the new outer width (including margins, borders and paddings)
+ * of the component in pixels
+ */
+ public void reportOuterWidth(ComponentConnector component, int outerWidth) {
+ MeasuredSize measuredSize = getMeasuredSize(component);
+ if (isLayoutRunning()) {
+ boolean widthChanged = measuredSize.setOuterWidth(outerWidth);
+
+ if (widthChanged) {
+ onConnectorChange(component, true, false);
+ notifyListenersAndDepdendents(component.getWidget()
+ .getElement(), true, false);
+ }
+ currentDependencyTree.setNeedsHorizontalMeasure(component, false);
+ } else if (measuredSize.getOuterWidth() != outerWidth) {
+ setNeedsMeasure(component);
+ }
+ }
+
+ /**
+ * Adds a listener that will be notified whenever the size of a specific
+ * element changes. Adding a listener to an element also ensures that all
+ * sizes for that element will be available starting from the next layout
+ * phase.
+ *
+ * @param element
+ * the element that should be checked for size changes
+ * @param listener
+ * an ElementResizeListener that will be informed whenever the
+ * size of the target element has changed
+ */
+ public void addElementResizeListener(Element element,
+ ElementResizeListener listener) {
+ Collection<ElementResizeListener> listeners = elementResizeListeners
+ .get(element);
+ if (listeners == null) {
+ listeners = new HashSet<ElementResizeListener>();
+ elementResizeListeners.put(element, listeners);
+ ensureMeasured(element);
+ }
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes an element resize listener from the provided element. This might
+ * cause this LayoutManager to stop tracking the size of the element if no
+ * other sources are interested in the size.
+ *
+ * @param element
+ * the element to which the element resize listener was
+ * previously added
+ * @param listener
+ * the ElementResizeListener that should no longer get informed
+ * about size changes to the target element.
+ */
+ public void removeElementResizeListener(Element element,
+ ElementResizeListener listener) {
+ Collection<ElementResizeListener> listeners = elementResizeListeners
+ .get(element);
+ if (listeners != null) {
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ elementResizeListeners.remove(element);
+ stopMeasuringIfUnecessary(element);
+ }
+ }
+ }
+
+ private void stopMeasuringIfUnecessary(Element element) {
+ if (!needsMeasure(element)) {
+ measuredNonConnectorElements.remove(element);
+ setMeasuredSize(element, null);
+ }
+ }
+
+ /**
+ * Informs this LayoutManager that the size of a component might have
+ * changed. If there is no upcoming layout phase, a new layout phase is
+ * scheduled. This method should be used whenever a size might have changed
+ * from outside of Vaadin's normal update phase, e.g. when an icon has been
+ * loaded or when the user resizes some part of the UI using the mouse.
+ *
+ * @param component
+ * the component whose size might have changed.
+ */
+ public void setNeedsMeasure(ComponentConnector component) {
+ if (isLayoutRunning()) {
+ currentDependencyTree.setNeedsMeasure(component, true);
+ } else {
+ needsMeasure.add(component);
+ layoutLater();
+ }
+ }
+
+ public void setEverythingNeedsMeasure() {
+ everythingNeedsMeasure = true;
+ }
+
+ /**
+ * Clean measured sizes which are no longer needed. Only for IE8.
+ */
+ protected void cleanMeasuredSizes() {
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java b/client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java
new file mode 100644
index 0000000000..ea130779ea
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/LayoutManagerIE8.java
@@ -0,0 +1,50 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.RootPanel;
+
+public class LayoutManagerIE8 extends LayoutManager {
+
+ private Map<Element, MeasuredSize> measuredSizes = new HashMap<Element, MeasuredSize>();
+
+ @Override
+ protected void setMeasuredSize(Element element, MeasuredSize measuredSize) {
+ if (measuredSize != null) {
+ measuredSizes.put(element, measuredSize);
+ } else {
+ measuredSizes.remove(element);
+ }
+ }
+
+ @Override
+ protected MeasuredSize getMeasuredSize(Element element,
+ MeasuredSize defaultSize) {
+ MeasuredSize measured = measuredSizes.get(element);
+ if (measured != null) {
+ return measured;
+ } else {
+ return defaultSize;
+ }
+ }
+
+ @Override
+ protected void cleanMeasuredSizes() {
+ Document document = RootPanel.get().getElement().getOwnerDocument();
+
+ Iterator<Element> i = measuredSizes.keySet().iterator();
+ while (i.hasNext()) {
+ Element e = i.next();
+ if (e.getOwnerDocument() != document) {
+ i.remove();
+ }
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java b/client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java
new file mode 100644
index 0000000000..871495c79e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java
@@ -0,0 +1,13 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+@SuppressWarnings("serial")
+public class LocaleNotLoadedException extends Exception {
+
+ public LocaleNotLoadedException(String locale) {
+ super(locale);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/LocaleService.java b/client/src/com/vaadin/terminal/gwt/client/LocaleService.java
new file mode 100644
index 0000000000..0f22ae705b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/LocaleService.java
@@ -0,0 +1,148 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.core.client.JsArray;
+
+/**
+ * Date / time etc. localisation service for all widgets. Caches all loaded
+ * locales as JSONObjects.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+public class LocaleService {
+
+ private static Map<String, ValueMap> cache = new HashMap<String, ValueMap>();
+ private static String defaultLocale;
+
+ public static void addLocale(ValueMap valueMap) {
+
+ final String key = valueMap.getString("name");
+ if (cache.containsKey(key)) {
+ cache.remove(key);
+ }
+ cache.put(key, valueMap);
+ if (cache.size() == 1) {
+ setDefaultLocale(key);
+ }
+ }
+
+ public static void setDefaultLocale(String locale) {
+ defaultLocale = locale;
+ }
+
+ public static String getDefaultLocale() {
+ return defaultLocale;
+ }
+
+ public static Set<String> getAvailableLocales() {
+ return cache.keySet();
+ }
+
+ public static String[] getMonthNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getStringArray("mn");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getShortMonthNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getStringArray("smn");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getDayNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getStringArray("dn");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getShortDayNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getStringArray("sdn");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static int getFirstDayOfWeek(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getInt("fdow");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String getDateFormat(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getString("df");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static boolean isTwelveHourClock(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getBoolean("thc");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String getClockDelimiter(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getString("hmd");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getAmPmStrings(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final ValueMap l = cache.get(locale);
+ return l.getStringArray("ampm");
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+
+ }
+
+ public static void addLocales(JsArray<ValueMap> valueMapArray) {
+ for (int i = 0; i < valueMapArray.length(); i++) {
+ addLocale(valueMapArray.get(i));
+
+ }
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java b/client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java
new file mode 100644
index 0000000000..97822fa8ec
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/MeasuredSize.java
@@ -0,0 +1,228 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+
+public class MeasuredSize {
+ public static class MeasureResult {
+ private final boolean widthChanged;
+ private final boolean heightChanged;
+
+ private MeasureResult(boolean widthChanged, boolean heightChanged) {
+ this.widthChanged = widthChanged;
+ this.heightChanged = heightChanged;
+ }
+
+ public boolean isHeightChanged() {
+ return heightChanged;
+ }
+
+ public boolean isWidthChanged() {
+ return widthChanged;
+ }
+
+ public boolean isChanged() {
+ return heightChanged || widthChanged;
+ }
+ }
+
+ private int width = -1;
+ private int height = -1;
+
+ private int[] paddings = new int[4];
+ private int[] borders = new int[4];
+ private int[] margins = new int[4];
+
+ private FastStringSet dependents = FastStringSet.create();
+
+ public int getOuterHeight() {
+ return height;
+ }
+
+ public int getOuterWidth() {
+ return width;
+ }
+
+ public void addDependent(String pid) {
+ dependents.add(pid);
+ }
+
+ public void removeDependent(String pid) {
+ dependents.remove(pid);
+ }
+
+ public boolean hasDependents() {
+ return !dependents.isEmpty();
+ }
+
+ public JsArrayString getDependents() {
+ return dependents.dump();
+ }
+
+ private static int sumWidths(int[] sizes) {
+ return sizes[1] + sizes[3];
+ }
+
+ private static int sumHeights(int[] sizes) {
+ return sizes[0] + sizes[2];
+ }
+
+ public int getInnerHeight() {
+ return height - sumHeights(margins) - sumHeights(borders)
+ - sumHeights(paddings);
+ }
+
+ public int getInnerWidth() {
+ return width - sumWidths(margins) - sumWidths(borders)
+ - sumWidths(paddings);
+ }
+
+ public boolean setOuterHeight(int height) {
+ if (this.height != height) {
+ this.height = height;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean setOuterWidth(int width) {
+ if (this.width != width) {
+ this.width = width;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public int getBorderHeight() {
+ return sumHeights(borders);
+ }
+
+ public int getBorderWidth() {
+ return sumWidths(borders);
+ }
+
+ public int getPaddingHeight() {
+ return sumHeights(paddings);
+ }
+
+ public int getPaddingWidth() {
+ return sumWidths(paddings);
+ }
+
+ public int getMarginHeight() {
+ return sumHeights(margins);
+ }
+
+ public int getMarginWidth() {
+ return sumWidths(margins);
+ }
+
+ public int getMarginTop() {
+ return margins[0];
+ }
+
+ public int getMarginRight() {
+ return margins[1];
+ }
+
+ public int getMarginBottom() {
+ return margins[2];
+ }
+
+ public int getMarginLeft() {
+ return margins[3];
+ }
+
+ public int getBorderTop() {
+ return margins[0];
+ }
+
+ public int getBorderRight() {
+ return margins[1];
+ }
+
+ public int getBorderBottom() {
+ return margins[2];
+ }
+
+ public int getBorderLeft() {
+ return margins[3];
+ }
+
+ public int getPaddingTop() {
+ return paddings[0];
+ }
+
+ public int getPaddingRight() {
+ return paddings[1];
+ }
+
+ public int getPaddingBottom() {
+ return paddings[2];
+ }
+
+ public int getPaddingLeft() {
+ return paddings[3];
+ }
+
+ public MeasureResult measure(Element element) {
+ boolean heightChanged = false;
+ boolean widthChanged = false;
+
+ ComputedStyle computedStyle = new ComputedStyle(element);
+ int[] paddings = computedStyle.getPadding();
+ if (!heightChanged && hasHeightChanged(this.paddings, paddings)) {
+ heightChanged = true;
+ }
+ if (!widthChanged && hasWidthChanged(this.paddings, paddings)) {
+ widthChanged = true;
+ }
+ this.paddings = paddings;
+
+ int[] margins = computedStyle.getMargin();
+ if (!heightChanged && hasHeightChanged(this.margins, margins)) {
+ heightChanged = true;
+ }
+ if (!widthChanged && hasWidthChanged(this.margins, margins)) {
+ widthChanged = true;
+ }
+ this.margins = margins;
+
+ int[] borders = computedStyle.getBorder();
+ if (!heightChanged && hasHeightChanged(this.borders, borders)) {
+ heightChanged = true;
+ }
+ if (!widthChanged && hasWidthChanged(this.borders, borders)) {
+ widthChanged = true;
+ }
+ this.borders = borders;
+
+ int requiredHeight = Util.getRequiredHeight(element);
+ int marginHeight = sumHeights(margins);
+ if (setOuterHeight(requiredHeight + marginHeight)) {
+ heightChanged = true;
+ }
+
+ int requiredWidth = Util.getRequiredWidth(element);
+ int marginWidth = sumWidths(margins);
+ if (setOuterWidth(requiredWidth + marginWidth)) {
+ widthChanged = true;
+ }
+
+ return new MeasureResult(widthChanged, heightChanged);
+ }
+
+ private static boolean hasWidthChanged(int[] sizes1, int[] sizes2) {
+ return sizes1[1] != sizes2[1] || sizes1[3] != sizes2[3];
+ }
+
+ private static boolean hasHeightChanged(int[] sizes1, int[] sizes2) {
+ return sizes1[0] != sizes2[0] || sizes1[2] != sizes2[2];
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java b/client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java
new file mode 100644
index 0000000000..d39f98a024
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/MouseEventDetailsBuilder.java
@@ -0,0 +1,75 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.Event;
+import com.vaadin.shared.MouseEventDetails;
+
+/**
+ * Helper class for constructing a MouseEventDetails object from a
+ * {@link NativeEvent}.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public class MouseEventDetailsBuilder {
+
+ /**
+ * Construct a {@link MouseEventDetails} object from the given event
+ *
+ * @param evt
+ * The event to use as a source for the details
+ * @return a MouseEventDetails containing information from the event
+ */
+ public static MouseEventDetails buildMouseEventDetails(NativeEvent evt) {
+ return buildMouseEventDetails(evt, null);
+ }
+
+ /**
+ * Construct a {@link MouseEventDetails} object from the given event
+ *
+ * @param evt
+ * The event to use as a source for the details
+ * @param relativeToElement
+ * The element whose position
+ * {@link MouseEventDetails#getRelativeX()} and
+ * {@link MouseEventDetails#getRelativeY()} are relative to.
+ * @return a MouseEventDetails containing information from the event
+ */
+ public static MouseEventDetails buildMouseEventDetails(NativeEvent evt,
+ Element relativeToElement) {
+ MouseEventDetails mouseEventDetails = new MouseEventDetails();
+ mouseEventDetails.setType(Event.getTypeInt(evt.getType()));
+ mouseEventDetails.setClientX(Util.getTouchOrMouseClientX(evt));
+ mouseEventDetails.setClientY(Util.getTouchOrMouseClientY(evt));
+ mouseEventDetails.setButton(evt.getButton());
+ mouseEventDetails.setAltKey(evt.getAltKey());
+ mouseEventDetails.setCtrlKey(evt.getCtrlKey());
+ mouseEventDetails.setMetaKey(evt.getMetaKey());
+ mouseEventDetails.setShiftKey(evt.getShiftKey());
+ if (relativeToElement != null) {
+ mouseEventDetails.setRelativeX(getRelativeX(
+ mouseEventDetails.getClientX(), relativeToElement));
+ mouseEventDetails.setRelativeY(getRelativeY(
+ mouseEventDetails.getClientY(), relativeToElement));
+ }
+ return mouseEventDetails;
+
+ }
+
+ private static int getRelativeX(int clientX, Element target) {
+ return clientX - target.getAbsoluteLeft() + target.getScrollLeft()
+ + target.getOwnerDocument().getScrollLeft();
+ }
+
+ private static int getRelativeY(int clientY, Element target) {
+ return clientY - target.getAbsoluteTop() + target.getScrollTop()
+ + target.getOwnerDocument().getScrollTop();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/NullConsole.java b/client/src/com/vaadin/terminal/gwt/client/NullConsole.java
new file mode 100644
index 0000000000..aba301d18d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/NullConsole.java
@@ -0,0 +1,63 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Set;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * Client side console implementation for non-debug mode that discards all
+ * messages.
+ *
+ */
+public class NullConsole implements Console {
+
+ @Override
+ public void dirUIDL(ValueMap u, ApplicationConnection conn) {
+ }
+
+ @Override
+ public void error(String msg) {
+ GWT.log(msg);
+ }
+
+ @Override
+ public void log(String msg) {
+ GWT.log(msg);
+ }
+
+ @Override
+ public void printObject(Object msg) {
+ GWT.log(msg.toString());
+ }
+
+ @Override
+ public void printLayoutProblems(ValueMap meta,
+ ApplicationConnection applicationConnection,
+ Set<ComponentConnector> zeroHeightComponents,
+ Set<ComponentConnector> zeroWidthComponents) {
+ }
+
+ @Override
+ public void log(Throwable e) {
+ GWT.log(e.getMessage(), e);
+ }
+
+ @Override
+ public void error(Throwable e) {
+ // Borrow exception handling from VDebugConsole
+ VDebugConsole.handleError(e, this);
+ }
+
+ @Override
+ public void setQuietMode(boolean quietDebugMode) {
+ }
+
+ @Override
+ public void init() {
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/Paintable.java b/client/src/com/vaadin/terminal/gwt/client/Paintable.java
new file mode 100644
index 0000000000..c9e3ef79cc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/Paintable.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+/**
+ * An interface used by client-side widgets or paintable parts to receive
+ * updates from the corresponding server-side components in the form of
+ * {@link UIDL}.
+ *
+ * Updates can be sent back to the server using the
+ * {@link ApplicationConnection#updateVariable()} methods.
+ */
+@Deprecated
+public interface Paintable {
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client);
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/RenderInformation.java b/client/src/com/vaadin/terminal/gwt/client/RenderInformation.java
new file mode 100644
index 0000000000..83d8abfd2f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/RenderInformation.java
@@ -0,0 +1,136 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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) {
+ if (!(obj instanceof Size)) {
+ return false;
+ }
+ Size other = (Size) obj;
+ return other.width == width && other.height == height;
+ }
+
+ @Override
+ public int hashCode() {
+ return (width << 8) | 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/client/src/com/vaadin/terminal/gwt/client/RenderSpace.java b/client/src/com/vaadin/terminal/gwt/client/RenderSpace.java
new file mode 100644
index 0000000000..fa28b0de95
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/RenderSpace.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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/client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java b/client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java
new file mode 100644
index 0000000000..21577ce87e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ResourceLoader.java
@@ -0,0 +1,540 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.LinkElement;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.ObjectElement;
+import com.google.gwt.dom.client.ScriptElement;
+import com.google.gwt.user.client.Timer;
+
+/**
+ * ResourceLoader lets you dynamically include external scripts and styles on
+ * the page and lets you know when the resource has been loaded.
+ *
+ * You can also preload resources, allowing them to get cached by the browser
+ * without being evaluated. This enables downloading multiple resources at once
+ * while still controlling in which order e.g. scripts are executed.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public class ResourceLoader {
+ /**
+ * Event fired when a resource has been loaded.
+ */
+ public static class ResourceLoadEvent {
+ private ResourceLoader loader;
+ private String resourceUrl;
+ private final boolean preload;
+
+ /**
+ * Creates a new event.
+ *
+ * @param loader
+ * the resource loader that has loaded the resource
+ * @param resourceUrl
+ * the url of the loaded resource
+ * @param preload
+ * true if the resource has only been preloaded, false if
+ * it's fully loaded
+ */
+ public ResourceLoadEvent(ResourceLoader loader, String resourceUrl,
+ boolean preload) {
+ this.loader = loader;
+ this.resourceUrl = resourceUrl;
+ this.preload = preload;
+ }
+
+ /**
+ * Gets the resource loader that has fired this event
+ *
+ * @return the resource loader
+ */
+ public ResourceLoader getResourceLoader() {
+ return loader;
+ }
+
+ /**
+ * Gets the absolute url of the loaded resource.
+ *
+ * @return the absolute url of the loaded resource
+ */
+ public String getResourceUrl() {
+ return resourceUrl;
+ }
+
+ /**
+ * Returns true if the resource has been preloaded, false if it's fully
+ * loaded
+ *
+ * @see ResourceLoader#preloadResource(String, ResourceLoadListener)
+ *
+ * @return true if the resource has been preloaded, false if it's fully
+ * loaded
+ */
+ public boolean isPreload() {
+ return preload;
+ }
+ }
+
+ /**
+ * Event listener that gets notified when a resource has been loaded
+ */
+ public interface ResourceLoadListener {
+ /**
+ * Notifies this ResourceLoadListener that a resource has been loaded.
+ * Some browsers do not support any way of detecting load errors. In
+ * these cases, onLoad will be called regardless of the status.
+ *
+ * @see ResourceLoadEvent
+ *
+ * @param event
+ * a resource load event with information about the loaded
+ * resource
+ */
+ public void onLoad(ResourceLoadEvent event);
+
+ /**
+ * Notifies this ResourceLoadListener that a resource could not be
+ * loaded, e.g. because the file could not be found or because the
+ * server did not respond. Some browsers do not support any way of
+ * detecting load errors. In these cases, onLoad will be called
+ * regardless of the status.
+ *
+ * @see ResourceLoadEvent
+ *
+ * @param event
+ * a resource load event with information about the resource
+ * that could not be loaded.
+ */
+ public void onError(ResourceLoadEvent event);
+ }
+
+ private static final ResourceLoader INSTANCE = GWT
+ .create(ResourceLoader.class);
+
+ private ApplicationConnection connection;
+
+ private final Set<String> loadedResources = new HashSet<String>();
+ private final Set<String> preloadedResources = new HashSet<String>();
+
+ private final Map<String, Collection<ResourceLoadListener>> loadListeners = new HashMap<String, Collection<ResourceLoadListener>>();
+ private final Map<String, Collection<ResourceLoadListener>> preloadListeners = new HashMap<String, Collection<ResourceLoadListener>>();
+
+ private final Element head;
+
+ /**
+ * Creates a new resource loader. You should generally not create you own
+ * resource loader, but instead use {@link ResourceLoader#get()} to get an
+ * instance.
+ */
+ protected ResourceLoader() {
+ Document document = Document.get();
+ head = document.getElementsByTagName("head").getItem(0);
+
+ // detect already loaded scripts and stylesheets
+ NodeList<Element> scripts = document.getElementsByTagName("script");
+ for (int i = 0; i < scripts.getLength(); i++) {
+ ScriptElement element = ScriptElement.as(scripts.getItem(i));
+ String src = element.getSrc();
+ if (src != null && src.length() != 0) {
+ loadedResources.add(src);
+ }
+ }
+
+ NodeList<Element> links = document.getElementsByTagName("link");
+ for (int i = 0; i < links.getLength(); i++) {
+ LinkElement linkElement = LinkElement.as(links.getItem(i));
+ String rel = linkElement.getRel();
+ String href = linkElement.getHref();
+ if ("stylesheet".equalsIgnoreCase(rel) && href != null
+ && href.length() != 0) {
+ loadedResources.add(href);
+ }
+ }
+ }
+
+ /**
+ * Returns the default ResourceLoader
+ *
+ * @return the default ResourceLoader
+ */
+ public static ResourceLoader get() {
+ return INSTANCE;
+ }
+
+ /**
+ * Load a script and notify a listener when the script is loaded. Calling
+ * this method when the script is currently loading or already loaded
+ * doesn't cause the script to be loaded again, but the listener will still
+ * be notified when appropriate.
+ *
+ *
+ * @param scriptUrl
+ * the url of the script to load
+ * @param resourceLoadListener
+ * the listener that will get notified when the script is loaded
+ */
+ public void loadScript(final String scriptUrl,
+ final ResourceLoadListener resourceLoadListener) {
+ final String url = getAbsoluteUrl(scriptUrl);
+ ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
+ if (loadedResources.contains(url)) {
+ if (resourceLoadListener != null) {
+ resourceLoadListener.onLoad(event);
+ }
+ return;
+ }
+
+ if (preloadListeners.containsKey(url)) {
+ // Preload going on, continue when preloaded
+ preloadResource(url, new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ loadScript(url, resourceLoadListener);
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ // Preload failed -> signal error to own listener
+ if (resourceLoadListener != null) {
+ resourceLoadListener.onError(event);
+ }
+ }
+ });
+ return;
+ }
+
+ if (addListener(url, resourceLoadListener, loadListeners)) {
+ ScriptElement scriptTag = Document.get().createScriptElement();
+ scriptTag.setSrc(url);
+ scriptTag.setType("text/javascript");
+ addOnloadHandler(scriptTag, new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ fireLoad(event);
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ fireError(event);
+ }
+ }, event);
+ head.appendChild(scriptTag);
+ }
+ }
+
+ private static String getAbsoluteUrl(String url) {
+ AnchorElement a = Document.get().createAnchorElement();
+ a.setHref(url);
+ return a.getHref();
+ }
+
+ /**
+ * Download a resource and notify a listener when the resource is loaded
+ * without attempting to interpret the resource. When a resource has been
+ * preloaded, it will be present in the browser's cache (provided the HTTP
+ * headers allow caching), making a subsequent load operation complete
+ * without having to wait for the resource to be downloaded again.
+ *
+ * Calling this method when the resource is currently loading, currently
+ * preloading, already preloaded or already loaded doesn't cause the
+ * resource to be preloaded again, but the listener will still be notified
+ * when appropriate.
+ *
+ * @param url
+ * the url of the resource to preload
+ * @param resourceLoadListener
+ * the listener that will get notified when the resource is
+ * preloaded
+ */
+ public void preloadResource(String url,
+ ResourceLoadListener resourceLoadListener) {
+ url = getAbsoluteUrl(url);
+ ResourceLoadEvent event = new ResourceLoadEvent(this, url, true);
+ if (loadedResources.contains(url) || preloadedResources.contains(url)) {
+ // Already loaded or preloaded -> just fire listener
+ if (resourceLoadListener != null) {
+ resourceLoadListener.onLoad(event);
+ }
+ return;
+ }
+
+ if (addListener(url, resourceLoadListener, preloadListeners)
+ && !loadListeners.containsKey(url)) {
+ // Inject loader element if this is the first time this is preloaded
+ // AND the resources isn't already being loaded in the normal way
+
+ Element element = getPreloadElement(url);
+ addOnloadHandler(element, new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ fireLoad(event);
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ fireError(event);
+ }
+ }, event);
+
+ // TODO Remove object when loaded (without causing spinner in FF)
+ Document.get().getBody().appendChild(element);
+ }
+ }
+
+ private static Element getPreloadElement(String url) {
+ if (BrowserInfo.get().isIE()) {
+ ScriptElement element = Document.get().createScriptElement();
+ element.setSrc(url);
+ element.setType("text/cache");
+ return element;
+ } else {
+ ObjectElement element = Document.get().createObjectElement();
+ element.setData(url);
+ element.setType("text/plain");
+ element.setHeight("0px");
+ element.setWidth("0px");
+ return element;
+ }
+ }
+
+ private native void addOnloadHandler(Element element,
+ ResourceLoadListener listener, ResourceLoadEvent event)
+ /*-{
+ element.onload = $entry(function() {
+ element.onload = null;
+ element.onerror = null;
+ element.onreadystatechange = null;
+ listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onLoad(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event);
+ });
+ element.onerror = $entry(function() {
+ element.onload = null;
+ element.onerror = null;
+ element.onreadystatechange = null;
+ listener.@com.vaadin.terminal.gwt.client.ResourceLoader.ResourceLoadListener::onError(Lcom/vaadin/terminal/gwt/client/ResourceLoader$ResourceLoadEvent;)(event);
+ });
+ element.onreadystatechange = function() {
+ if ("loaded" === element.readyState || "complete" === element.readyState ) {
+ element.onload(arguments[0]);
+ }
+ };
+ }-*/;
+
+ /**
+ * Load a stylesheet and notify a listener when the stylesheet is loaded.
+ * Calling this method when the stylesheet is currently loading or already
+ * loaded doesn't cause the stylesheet to be loaded again, but the listener
+ * will still be notified when appropriate.
+ *
+ * @param stylesheetUrl
+ * the url of the stylesheet to load
+ * @param resourceLoadListener
+ * the listener that will get notified when the stylesheet is
+ * loaded
+ */
+ public void loadStylesheet(final String stylesheetUrl,
+ final ResourceLoadListener resourceLoadListener) {
+ final String url = getAbsoluteUrl(stylesheetUrl);
+ final ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
+ if (loadedResources.contains(url)) {
+ if (resourceLoadListener != null) {
+ resourceLoadListener.onLoad(event);
+ }
+ return;
+ }
+
+ if (preloadListeners.containsKey(url)) {
+ // Preload going on, continue when preloaded
+ preloadResource(url, new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ loadStylesheet(url, resourceLoadListener);
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ // Preload failed -> signal error to own listener
+ if (resourceLoadListener != null) {
+ resourceLoadListener.onError(event);
+ }
+ }
+ });
+ return;
+ }
+
+ if (addListener(url, resourceLoadListener, loadListeners)) {
+ LinkElement linkElement = Document.get().createLinkElement();
+ linkElement.setRel("stylesheet");
+ linkElement.setType("text/css");
+ linkElement.setHref(url);
+
+ if (BrowserInfo.get().isSafari()) {
+ // Safari doesn't fire any events for link elements
+ // See http://www.phpied.com/when-is-a-stylesheet-really-loaded/
+ Scheduler.get().scheduleFixedPeriod(new RepeatingCommand() {
+ private final Duration duration = new Duration();
+
+ @Override
+ public boolean execute() {
+ int styleSheetLength = getStyleSheetLength(url);
+ if (getStyleSheetLength(url) > 0) {
+ fireLoad(event);
+ return false; // Stop repeating
+ } else if (styleSheetLength == 0) {
+ // "Loaded" empty sheet -> most likely 404 error
+ fireError(event);
+ return true;
+ } else if (duration.elapsedMillis() > 60 * 1000) {
+ fireError(event);
+ return false;
+ } else {
+ return true; // Continue repeating
+ }
+ }
+ }, 10);
+ } else {
+ addOnloadHandler(linkElement, new ResourceLoadListener() {
+ @Override
+ public void onLoad(ResourceLoadEvent event) {
+ // Chrome && IE fires load for errors, must check
+ // stylesheet data
+ if (BrowserInfo.get().isChrome()
+ || BrowserInfo.get().isIE()) {
+ int styleSheetLength = getStyleSheetLength(url);
+ // Error if there's an empty stylesheet
+ if (styleSheetLength == 0) {
+ fireError(event);
+ return;
+ }
+ }
+ fireLoad(event);
+ }
+
+ @Override
+ public void onError(ResourceLoadEvent event) {
+ fireError(event);
+ }
+ }, event);
+ if (BrowserInfo.get().isOpera()) {
+ // Opera onerror never fired, assume error if no onload in x
+ // seconds
+ new Timer() {
+ @Override
+ public void run() {
+ if (!loadedResources.contains(url)) {
+ fireError(event);
+ }
+ }
+ }.schedule(5 * 1000);
+ }
+ }
+
+ head.appendChild(linkElement);
+ }
+ }
+
+ private static native int getStyleSheetLength(String url)
+ /*-{
+ for(var i = 0; i < $doc.styleSheets.length; i++) {
+ if ($doc.styleSheets[i].href === url) {
+ var sheet = $doc.styleSheets[i];
+ try {
+ var rules = sheet.cssRules
+ if (rules === undefined) {
+ rules = sheet.rules;
+ }
+
+ if (rules === null) {
+ // Style sheet loaded, but can't access length because of XSS -> assume there's something there
+ return 1;
+ }
+
+ // Return length so we can distinguish 0 (probably 404 error) from normal case.
+ return rules.length;
+ } catch (err) {
+ return 1;
+ }
+ }
+ }
+ // No matching stylesheet found -> not yet loaded
+ return -1;
+ }-*/;
+
+ private static boolean addListener(String url,
+ ResourceLoadListener listener,
+ Map<String, Collection<ResourceLoadListener>> listenerMap) {
+ Collection<ResourceLoadListener> listeners = listenerMap.get(url);
+ if (listeners == null) {
+ listeners = new HashSet<ResourceLoader.ResourceLoadListener>();
+ listeners.add(listener);
+ listenerMap.put(url, listeners);
+ return true;
+ } else {
+ listeners.add(listener);
+ return false;
+ }
+ }
+
+ private void fireError(ResourceLoadEvent event) {
+ String resource = event.getResourceUrl();
+
+ Collection<ResourceLoadListener> listeners;
+ if (event.isPreload()) {
+ // Also fire error for load listeners
+ fireError(new ResourceLoadEvent(this, resource, false));
+ listeners = preloadListeners.remove(resource);
+ } else {
+ listeners = loadListeners.remove(resource);
+ }
+ if (listeners != null && !listeners.isEmpty()) {
+ for (ResourceLoadListener listener : listeners) {
+ if (listener != null) {
+ listener.onError(event);
+ }
+ }
+ }
+ }
+
+ private void fireLoad(ResourceLoadEvent event) {
+ String resource = event.getResourceUrl();
+ Collection<ResourceLoadListener> listeners;
+ if (event.isPreload()) {
+ preloadedResources.add(resource);
+ listeners = preloadListeners.remove(resource);
+ } else {
+ if (preloadListeners.containsKey(resource)) {
+ // Also fire preload events for potential listeners
+ fireLoad(new ResourceLoadEvent(this, resource, true));
+ }
+ preloadedResources.remove(resource);
+ loadedResources.add(resource);
+ listeners = loadListeners.remove(resource);
+ }
+ if (listeners != null && !listeners.isEmpty()) {
+ for (ResourceLoadListener listener : listeners) {
+ if (listener != null) {
+ listener.onLoad(event);
+ }
+ }
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ServerConnector.java b/client/src/com/vaadin/terminal/gwt/client/ServerConnector.java
new file mode 100644
index 0000000000..ce46a3fb5e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ServerConnector.java
@@ -0,0 +1,119 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Collection;
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler;
+
+/**
+ * Interface implemented by all client side classes that can be communicate with
+ * the server. Classes implementing this interface are initialized by the
+ * framework when needed and have the ability to communicate with the server.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public interface ServerConnector extends Connector {
+
+ /**
+ * Gets ApplicationConnection instance that created this connector.
+ *
+ * @return The ApplicationConnection as set by
+ * {@link #doInit(String, ApplicationConnection)}
+ */
+ public ApplicationConnection getConnection();
+
+ /**
+ * Tests whether the connector is enabled or not. This method checks that
+ * the connector is enabled in context, i.e. if the parent connector is
+ * disabled, this method must return false.
+ *
+ * @return true if the connector is enabled, false otherwise
+ */
+ public boolean isEnabled();
+
+ /**
+ *
+ * Called once by the framework to initialize the connector.
+ * <p>
+ * Note that the shared state is not yet available at this point nor any
+ * hierarchy information.
+ */
+ public void doInit(String connectorId, ApplicationConnection connection);
+
+ /**
+ * For internal use by the framework: returns the registered RPC
+ * implementations for an RPC interface identifier.
+ *
+ * TODO interface identifier type or format may change
+ *
+ * @param rpcInterfaceId
+ * RPC interface identifier: fully qualified interface type name
+ * @return RPC interface implementations registered for an RPC interface,
+ * not null
+ */
+ public <T extends ClientRpc> Collection<T> getRpcImplementations(
+ String rpcInterfaceId);
+
+ /**
+ * Adds a handler that is called whenever some part of the state has been
+ * updated by the server.
+ *
+ * @param handler
+ * The handler that should be added.
+ * @return A handler registration reference that can be used to unregister
+ * the handler
+ */
+ public HandlerRegistration addStateChangeHandler(StateChangeHandler handler);
+
+ /**
+ * Sends the given event to all registered handlers.
+ *
+ * @param event
+ * The event to send.
+ */
+ public void fireEvent(GwtEvent<?> event);
+
+ /**
+ * Event called when connector has been unregistered.
+ */
+ public void onUnregister();
+
+ /**
+ * Returns the parent of this connector. Can be null for only the root
+ * connector.
+ *
+ * @return The parent of this connector, as set by
+ * {@link #setParent(ServerConnector)}.
+ */
+ @Override
+ public ServerConnector getParent();
+
+ /**
+ * Sets the parent for this connector. This method should only be called by
+ * the framework to ensure that the connector hierarchy on the client side
+ * and the server side are in sync.
+ * <p>
+ * Note that calling this method does not fire a
+ * {@link ConnectorHierarchyChangeEvent}. The event is fired only when the
+ * whole hierarchy has been updated.
+ *
+ * @param parent
+ * The new parent of the connector
+ */
+ public void setParent(ServerConnector parent);
+
+ public void updateEnabledState(boolean enabledState);
+
+ public void setChildren(List<ServerConnector> children);
+
+ public List<ServerConnector> getChildren();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/SimpleTree.java b/client/src/com/vaadin/terminal/gwt/client/SimpleTree.java
new file mode 100644
index 0000000000..506d990aac
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/SimpleTree.java
@@ -0,0 +1,117 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.BorderStyle;
+import com.google.gwt.dom.client.Style.Cursor;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+
+public class SimpleTree extends ComplexPanel {
+ private Element children = Document.get().createDivElement().cast();
+ private SpanElement handle = Document.get().createSpanElement();
+ private SpanElement text = Document.get().createSpanElement();
+
+ public SimpleTree() {
+ setElement(Document.get().createDivElement());
+ Style style = getElement().getStyle();
+ style.setProperty("whiteSpace", "nowrap");
+ style.setPadding(3, Unit.PX);
+ style.setPaddingLeft(12, Unit.PX);
+
+ style = handle.getStyle();
+ style.setDisplay(Display.NONE);
+ style.setProperty("textAlign", "center");
+ style.setWidth(10, Unit.PX);
+ style.setCursor(Cursor.POINTER);
+ style.setBorderStyle(BorderStyle.SOLID);
+ style.setBorderColor("#666");
+ style.setBorderWidth(1, Unit.PX);
+ style.setMarginRight(3, Unit.PX);
+ style.setProperty("borderRadius", "4px");
+ handle.setInnerHTML("+");
+ getElement().appendChild(handle);
+ getElement().appendChild(text);
+ style = children.getStyle();
+ style.setPaddingLeft(9, Unit.PX);
+ style.setDisplay(Display.NONE);
+
+ getElement().appendChild(children);
+ addDomHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (event.getNativeEvent().getEventTarget().cast() == handle) {
+ if (children.getStyle().getDisplay().intern() == Display.NONE
+ .getCssName()) {
+ open(event.getNativeEvent().getShiftKey());
+ } else {
+ close();
+ }
+
+ } else if (event.getNativeEvent().getEventTarget().cast() == text) {
+ select(event);
+ }
+ }
+ }, ClickEvent.getType());
+ }
+
+ protected void select(ClickEvent event) {
+
+ }
+
+ public void close() {
+ children.getStyle().setDisplay(Display.NONE);
+ handle.setInnerHTML("+");
+ }
+
+ public void open(boolean recursive) {
+ handle.setInnerHTML("-");
+ children.getStyle().setDisplay(Display.BLOCK);
+ if (recursive) {
+ for (Widget w : getChildren()) {
+ if (w instanceof SimpleTree) {
+ SimpleTree str = (SimpleTree) w;
+ str.open(true);
+ }
+ }
+ }
+ }
+
+ public SimpleTree(String caption) {
+ this();
+ setText(caption);
+ }
+
+ public void setText(String text) {
+ this.text.setInnerText(text);
+ }
+
+ public void addItem(String text) {
+ Label label = new Label(text);
+ add(label, children);
+ }
+
+ @Override
+ public void add(Widget child) {
+ add(child, children);
+ }
+
+ @Override
+ protected void add(Widget child, Element container) {
+ super.add(child, container);
+ handle.getStyle().setDisplay(Display.INLINE_BLOCK);
+ getElement().getStyle().setPaddingLeft(3, Unit.PX);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/StyleConstants.java b/client/src/com/vaadin/terminal/gwt/client/StyleConstants.java
new file mode 100644
index 0000000000..0809580076
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/StyleConstants.java
@@ -0,0 +1,17 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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/client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java b/client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java
new file mode 100644
index 0000000000..e435b3c6ed
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/SuperDevMode.java
@@ -0,0 +1,253 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.UrlBuilder;
+import com.google.gwt.jsonp.client.JsonpRequestBuilder;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification.EventListener;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification.HideEvent;
+
+/**
+ * Class that enables SuperDevMode using a ?superdevmode parameter in the url.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0
+ *
+ */
+public class SuperDevMode {
+
+ private static final int COMPILE_TIMEOUT_IN_SECONDS = 60;
+ protected static final String SKIP_RECOMPILE = "VaadinSuperDevMode_skip_recompile";
+
+ public static class RecompileResult extends JavaScriptObject {
+ protected RecompileResult() {
+
+ }
+
+ public final native boolean ok()
+ /*-{
+ return this.status == "ok";
+ }-*/;
+ }
+
+ private static void recompileWidgetsetAndStartInDevMode(
+ final String serverUrl) {
+ VConsole.log("Recompiling widgetset using<br/>" + serverUrl
+ + "<br/>and then reloading in super dev mode");
+ VNotification n = new VNotification();
+ n.show("<b>Recompiling widgetset, this should not take too long</b>",
+ VNotification.CENTERED, VNotification.STYLE_SYSTEM);
+
+ JsonpRequestBuilder b = new JsonpRequestBuilder();
+ b.setCallbackParam("_callback");
+ b.setTimeout(COMPILE_TIMEOUT_IN_SECONDS * 1000);
+ b.requestObject(serverUrl + "recompile/" + GWT.getModuleName() + "?"
+ + getRecompileParameters(GWT.getModuleName()),
+ new AsyncCallback<RecompileResult>() {
+
+ @Override
+ public void onSuccess(RecompileResult result) {
+ VConsole.log("JSONP compile call successful");
+
+ if (!result.ok()) {
+ VConsole.log("* result: " + result);
+ failed();
+ return;
+ }
+
+ setSession(
+ getSuperDevModeHookKey(),
+ getSuperDevWidgetSetUrl(GWT.getModuleName(),
+ serverUrl));
+ setSession(SKIP_RECOMPILE, "1");
+
+ VConsole.log("* result: OK. Reloading");
+ Location.reload();
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ VConsole.error("JSONP compile call failed");
+ // Don't log exception as they are shown as
+ // notifications
+ VConsole.error(Util.getSimpleName(caught) + ": "
+ + caught.getMessage());
+ failed();
+
+ }
+
+ private void failed() {
+ VNotification n = new VNotification();
+ n.addEventListener(new EventListener() {
+
+ @Override
+ public void notificationHidden(HideEvent event) {
+ recompileWidgetsetAndStartInDevMode(serverUrl);
+ }
+ });
+ n.show("Recompilation failed.<br/>"
+ + "Make sure CodeServer is running, "
+ + "check its output and click to retry",
+ VNotification.CENTERED,
+ VNotification.STYLE_SYSTEM);
+ }
+ });
+
+ }
+
+ protected static String getSuperDevWidgetSetUrl(String widgetsetName,
+ String serverUrl) {
+ return serverUrl + GWT.getModuleName() + "/" + GWT.getModuleName()
+ + ".nocache.js";
+ }
+
+ private native static String getRecompileParameters(String moduleName)
+ /*-{
+ var prop_map = $wnd.__gwt_activeModules[moduleName].bindings();
+
+ // convert map to URL parameter string
+ var props = [];
+ for (var key in prop_map) {
+ props.push(encodeURIComponent(key) + '=' + encodeURIComponent(prop_map[key]))
+ }
+
+ return props.join('&') + '&';
+ }-*/;
+
+ private static void setSession(String key, String value) {
+ Storage.getSessionStorageIfSupported().setItem(key, value);
+ }
+
+ private static String getSession(String key) {
+ return Storage.getSessionStorageIfSupported().getItem(key);
+ }
+
+ private static void removeSession(String key) {
+ Storage.getSessionStorageIfSupported().removeItem(key);
+ }
+
+ protected static void disableDevModeAndReload() {
+ removeSession(getSuperDevModeHookKey());
+ redirect(false);
+ }
+
+ protected static void redirect(boolean devModeOn) {
+ UrlBuilder createUrlBuilder = Location.createUrlBuilder();
+ if (!devModeOn) {
+ createUrlBuilder.removeParameter("superdevmode");
+ } else {
+ createUrlBuilder.setParameter("superdevmode", "");
+ }
+
+ Location.assign(createUrlBuilder.buildString());
+
+ }
+
+ private static String getSuperDevModeHookKey() {
+ String widgetsetName = GWT.getModuleName();
+ final String superDevModeKey = "__gwtDevModeHook:" + widgetsetName;
+ return superDevModeKey;
+ }
+
+ private static boolean hasSession(String key) {
+ return getSession(key) != null;
+ }
+
+ /**
+ * The URL of the code server. The default URL (http://localhost:9876/) will
+ * be used if this is empty or null.
+ *
+ * @param serverUrl
+ * The url of the code server or null to use the default
+ * @return true if recompile started, false if we are running in
+ * SuperDevMode
+ */
+ protected static boolean recompileIfNeeded(String serverUrl) {
+ if (serverUrl == null || "".equals(serverUrl)) {
+ serverUrl = "http://localhost:9876/";
+ } else {
+ serverUrl = "http://" + serverUrl + "/";
+ }
+
+ if (hasSession(SKIP_RECOMPILE)) {
+ VConsole.log("Running in SuperDevMode");
+ // When we get here, we are running in super dev mode
+
+ // Remove the flag so next reload will recompile
+ removeSession(SKIP_RECOMPILE);
+
+ // Remove the gwt flag so we will not end up in dev mode if we
+ // remove the url parameter manually
+ removeSession(getSuperDevModeHookKey());
+
+ return false;
+ }
+
+ recompileWidgetsetAndStartInDevMode(serverUrl);
+ return true;
+ }
+
+ protected static boolean isSuperDevModeEnabledInModule() {
+ String moduleName = GWT.getModuleName();
+ return isSuperDevModeEnabledInModule(moduleName);
+ }
+
+ protected native static boolean isSuperDevModeEnabledInModule(
+ String moduleName)
+ /*-{
+ if (!$wnd.__gwt_activeModules)
+ return false;
+ var mod = $wnd.__gwt_activeModules[moduleName];
+ if (!mod)
+ return false;
+
+ if (mod.superdevmode) {
+ // Running in super dev mode already, it is supported
+ return true;
+ }
+
+ return !!mod.canRedirect;
+ }-*/;
+
+ /**
+ * Enables SuperDevMode if the url contains the "superdevmode" parameter.
+ * <p>
+ * The caller should not continue initialization of the application if this
+ * method returns true. The application will be restarted once compilation
+ * is done and then this method will return false.
+ * </p>
+ *
+ * @return true if a recompile operation has started and the page will be
+ * reloaded once it is done, false if no recompilation will be done.
+ */
+ public static boolean enableBasedOnParameter() {
+ String superDevModeParameter = Location.getParameter("superdevmode");
+ if (superDevModeParameter != null) {
+ // Need to check the recompile flag also because if we are running
+ // in super dev mode, as a result of the recompile, the enabled
+ // check will fail...
+ if (!isSuperDevModeEnabledInModule()) {
+ showError("SuperDevMode is not enabled for this module/widgetset.<br/>"
+ + "Ensure that your module definition (.gwt.xml) contains <br/>"
+ + "&lt;add-linker name=&quot;xsiframe&quot;/&gt;<br/>"
+ + "&lt;set-configuration-property name=&quot;devModeRedirectEnabled&quot; value=&quot;true&quot; /&gt;<br/>");
+ return false;
+ }
+ return SuperDevMode.recompileIfNeeded(superDevModeParameter);
+ }
+ return false;
+ }
+
+ private static void showError(String message) {
+ VNotification n = new VNotification();
+ n.show(message, VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java b/client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java
new file mode 100644
index 0000000000..6d9ad4a083
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java
@@ -0,0 +1,24 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.xhr.client.XMLHttpRequest;
+
+public class SynchronousXHR extends XMLHttpRequest {
+
+ protected SynchronousXHR() {
+ }
+
+ public native final void synchronousPost(String uri, String requestData)
+ /*-{
+ try {
+ this.open("POST", uri, false);
+ this.setRequestHeader("Content-Type", "text/plain;charset=utf-8");
+ this.send(requestData);
+ } catch (e) {
+ // No errors are managed as this is synchronous forceful send that can just fail
+ }
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java b/client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java
new file mode 100644
index 0000000000..712d263695
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/TooltipInfo.java
@@ -0,0 +1,54 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+public class TooltipInfo {
+
+ private String title;
+
+ private String errorMessageHtml;
+
+ public TooltipInfo() {
+ }
+
+ public TooltipInfo(String tooltip) {
+ setTitle(tooltip);
+ }
+
+ public TooltipInfo(String tooltip, String errorMessage) {
+ setTitle(tooltip);
+ setErrorMessage(errorMessage);
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getErrorMessage() {
+ return errorMessageHtml;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ errorMessageHtml = errorMessage;
+ }
+
+ /**
+ * Checks is a message has been defined for the tooltip.
+ *
+ * @return true if title or error message is present, false if both are
+ * empty
+ */
+ public boolean hasMessage() {
+ return (title != null && !title.isEmpty())
+ || (errorMessageHtml != null && !errorMessageHtml.isEmpty());
+ }
+
+ public boolean equals(TooltipInfo other) {
+ return (other != null && other.title == title && other.errorMessageHtml == errorMessageHtml);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/UIDL.java b/client/src/com/vaadin/terminal/gwt/client/UIDL.java
new file mode 100644
index 0000000000..e662e08b3f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/UIDL.java
@@ -0,0 +1,551 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Component;
+
+/**
+ * When a component is updated, it's client side widget's
+ * {@link ComponentConnector#updateFromUIDL(UIDL, ApplicationConnection)
+ * updateFromUIDL()} will be called with the updated ("changes") UIDL received
+ * from the server.
+ * <p>
+ * UIDL is hierarchical, and there are a few methods to retrieve the children,
+ * {@link #getChildCount()}, {@link #getChildIterator()}
+ * {@link #getChildString(int)}, {@link #getChildUIDL(int)}.
+ * </p>
+ * <p>
+ * It can be helpful to keep in mind that UIDL was originally modeled in XML, so
+ * it's structure is very XML -like. For instance, the first to children in the
+ * underlying UIDL representation will contain the "tag" name and attributes,
+ * but will be skipped by the methods mentioned above.
+ * </p>
+ */
+public final class UIDL extends JavaScriptObject {
+
+ protected UIDL() {
+ }
+
+ /**
+ * Shorthand for getting the attribute named "id", which for Paintables is
+ * the essential paintableId which binds the server side component to the
+ * client side widget.
+ *
+ * @return the value of the id attribute, if available
+ */
+ public String getId() {
+ return getStringAttribute("id");
+ }
+
+ /**
+ * Gets the name of this UIDL section, as created with
+ * {@link PaintTarget#startTag(String) PaintTarget.startTag()} in the
+ * server-side {@link Component#paint(PaintTarget) Component.paint()} or
+ * (usually) {@link AbstractComponent#paintContent(PaintTarget)
+ * AbstractComponent.paintContent()}. Note that if the UIDL corresponds to a
+ * Paintable, a component identifier will be returned instead - this is used
+ * internally and is not needed within
+ * {@link ComponentConnector#updateFromUIDL(UIDL, ApplicationConnection)
+ * updateFromUIDL()}.
+ *
+ * @return the name for this section
+ */
+ public native String getTag()
+ /*-{
+ return this[0];
+ }-*/;
+
+ private native ValueMap attr()
+ /*-{
+ return this[1];
+ }-*/;
+
+ private native ValueMap var()
+ /*-{
+ return this[1]["v"];
+ }-*/;
+
+ private native boolean hasVariables()
+ /*-{
+ return Boolean(this[1]["v"]);
+ }-*/;
+
+ /**
+ * Gets the named attribute as a String.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public String getStringAttribute(String name) {
+ return attr().getString(name);
+ }
+
+ /**
+ * Gets the names of the attributes available.
+ *
+ * @return the names of available attributes
+ */
+ public Set<String> getAttributeNames() {
+ Set<String> keySet = attr().getKeySet();
+ keySet.remove("v");
+ return keySet;
+ }
+
+ /**
+ * Gets the names of variables available.
+ *
+ * @return the names of available variables
+ */
+ public Set<String> getVariableNames() {
+ if (!hasVariables()) {
+ return new HashSet<String>();
+ } else {
+ Set<String> keySet = var().getKeySet();
+ return keySet;
+ }
+ }
+
+ /**
+ * Gets the named attribute as an int.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public int getIntAttribute(String name) {
+ return attr().getInt(name);
+ }
+
+ /**
+ * Gets the named attribute as a long.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public long getLongAttribute(String name) {
+ return (long) attr().getRawNumber(name);
+ }
+
+ /**
+ * Gets the named attribute as a float.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public float getFloatAttribute(String name) {
+ return (float) attr().getRawNumber(name);
+ }
+
+ /**
+ * Gets the named attribute as a double.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public double getDoubleAttribute(String name) {
+ return attr().getRawNumber(name);
+ }
+
+ /**
+ * Gets the named attribute as a boolean.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public boolean getBooleanAttribute(String name) {
+ return attr().getBoolean(name);
+ }
+
+ /**
+ * Gets the named attribute as a Map of named values (key/value pairs).
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute Map
+ */
+ public ValueMap getMapAttribute(String name) {
+ return attr().getValueMap(name);
+ }
+
+ /**
+ * Gets the named attribute as an array of Strings.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public String[] getStringArrayAttribute(String name) {
+ return attr().getStringArray(name);
+ }
+
+ /**
+ * Gets the named attribute as an int array.
+ *
+ * @param name
+ * the name of the attribute to get
+ * @return the attribute value
+ */
+ public int[] getIntArrayAttribute(final String name) {
+ return attr().getIntArray(name);
+ }
+
+ /**
+ * Get attributes value as string whatever the type is
+ *
+ * @param name
+ * @return string presentation of attribute
+ */
+ native String getAttribute(String name)
+ /*-{
+ return '' + this[1][name];
+ }-*/;
+
+ native String getVariable(String name)
+ /*-{
+ return '' + this[1]['v'][name];
+ }-*/;
+
+ /**
+ * Indicates whether or not the named attribute is available.
+ *
+ * @param name
+ * the name of the attribute to check
+ * @return true if the attribute is available, false otherwise
+ */
+ public boolean hasAttribute(final String name) {
+ return attr().containsKey(name);
+ }
+
+ /**
+ * Gets the UIDL for the child at the given index.
+ *
+ * @param i
+ * the index of the child to get
+ * @return the UIDL of the child if it exists
+ */
+ public native UIDL getChildUIDL(int i)
+ /*-{
+ return this[i + 2];
+ }-*/;
+
+ /**
+ * Gets the child at the given index as a String.
+ *
+ * @param i
+ * the index of the child to get
+ * @return the String representation of the child if it exists
+ */
+ public native String getChildString(int i)
+ /*-{
+ return this[i + 2];
+ }-*/;
+
+ private native XML getChildXML(int index)
+ /*-{
+ return this[index + 2];
+ }-*/;
+
+ /**
+ * Gets an iterator that can be used to iterate trough the children of this
+ * UIDL.
+ * <p>
+ * The Object returned by <code>next()</code> will be appropriately typed -
+ * if it's UIDL, {@link #getTag()} can be used to check which section is in
+ * question.
+ * </p>
+ * <p>
+ * The basic use case is to iterate over the children of an UIDL update, and
+ * update the appropriate part of the widget for each child encountered, e.g
+ * if <code>getTag()</code> returns "color", one would update the widgets
+ * color to reflect the value of the "color" section.
+ * </p>
+ *
+ * @return an iterator for iterating over UIDL children
+ */
+ public Iterator<Object> getChildIterator() {
+
+ return new Iterator<Object>() {
+
+ int index = -1;
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object next() {
+
+ if (hasNext()) {
+ int typeOfChild = typeOfChild(++index);
+ switch (typeOfChild) {
+ case CHILD_TYPE_UIDL:
+ UIDL childUIDL = getChildUIDL(index);
+ return childUIDL;
+ case CHILD_TYPE_STRING:
+ return getChildString(index);
+ case CHILD_TYPE_XML:
+ return getChildXML(index);
+ default:
+ throw new IllegalStateException(
+ "Illegal child in tag " + getTag()
+ + " at index " + index);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasNext() {
+ int count = getChildCount();
+ return count > index + 1;
+ }
+
+ };
+ }
+
+ private static final int CHILD_TYPE_STRING = 0;
+ private static final int CHILD_TYPE_UIDL = 1;
+ private static final int CHILD_TYPE_XML = 2;
+
+ private native int typeOfChild(int index)
+ /*-{
+ var t = typeof this[index + 2];
+ if(t == "object") {
+ if(typeof(t.length) == "number") {
+ return 1;
+ } else {
+ return 2;
+ }
+ } else if (t == "string") {
+ return 0;
+ }
+ return -1;
+ }-*/;
+
+ /**
+ * @deprecated
+ */
+ @Deprecated
+ public String getChildrenAsXML() {
+ return toString();
+ }
+
+ /**
+ * Checks if the named variable is available.
+ *
+ * @param name
+ * the name of the variable desired
+ * @return true if the variable exists, false otherwise
+ */
+ public boolean hasVariable(String name) {
+ return hasVariables() && var().containsKey(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public String getStringVariable(String name) {
+ return var().getString(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public int getIntVariable(String name) {
+ return var().getInt(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public long getLongVariable(String name) {
+ return (long) var().getRawNumber(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public float getFloatVariable(String name) {
+ return (float) var().getRawNumber(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public double getDoubleVariable(String name) {
+ return var().getRawNumber(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public boolean getBooleanVariable(String name) {
+ return var().getBoolean(name);
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public String[] getStringArrayVariable(String name) {
+ return var().getStringArray(name);
+ }
+
+ /**
+ * Gets the value of the named String[] variable as a Set of Strings.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public Set<String> getStringArrayVariableAsSet(final String name) {
+ final HashSet<String> s = new HashSet<String>();
+ JsArrayString a = var().getJSStringArray(name);
+ for (int i = 0; i < a.length(); i++) {
+ s.add(a.get(i));
+ }
+ return s;
+ }
+
+ /**
+ * Gets the value of the named variable.
+ *
+ * @param name
+ * the name of the variable
+ * @return the value of the variable
+ */
+ public int[] getIntArrayVariable(String name) {
+ return var().getIntArray(name);
+ }
+
+ /**
+ * @deprecated should not be used anymore
+ */
+ @Deprecated
+ public final static class XML extends JavaScriptObject {
+ protected XML() {
+ }
+
+ public native String getXMLAsString()
+ /*-{
+ var buf = new Array();
+ var self = this;
+ for(j in self) {
+ buf.push("<");
+ buf.push(j);
+ buf.push(">");
+ buf.push(self[j]);
+ buf.push("</");
+ buf.push(j);
+ buf.push(">");
+ }
+ return buf.join("");
+ }-*/;
+ }
+
+ /**
+ * Returns the number of children.
+ *
+ * @return the number of children
+ */
+ public native int getChildCount()
+ /*-{
+ return this.length - 2;
+ }-*/;
+
+ native boolean isMapAttribute(String name)
+ /*-{
+ return typeof this[1][name] == "object";
+ }-*/;
+
+ /**
+ * Gets the Paintable with the id found in the named attributes's value.
+ *
+ * @param name
+ * the name of the attribute
+ * @return the Paintable referenced by the attribute, if it exists
+ */
+ public ServerConnector getPaintableAttribute(String name,
+ ApplicationConnection connection) {
+ return ConnectorMap.get(connection).getConnector(
+ getStringAttribute(name));
+ }
+
+ /**
+ * Gets the Paintable with the id found in the named variable's value.
+ *
+ * @param name
+ * the name of the variable
+ * @return the Paintable referenced by the variable, if it exists
+ */
+ public ServerConnector getPaintableVariable(String name,
+ ApplicationConnection connection) {
+ return ConnectorMap.get(connection).getConnector(
+ getStringVariable(name));
+ }
+
+ /**
+ * Returns the child UIDL by its name. If several child nodes exist with the
+ * given name, the first child UIDL will be returned.
+ *
+ * @param tagName
+ * @return the child UIDL or null if child wit given name was not found
+ */
+ public UIDL getChildByTagName(String tagName) {
+ Iterator<Object> childIterator = getChildIterator();
+ while (childIterator.hasNext()) {
+ Object next = childIterator.next();
+ if (next instanceof UIDL) {
+ UIDL childUIDL = (UIDL) next;
+ if (childUIDL.getTag().equals(tagName)) {
+ return childUIDL;
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/Util.java b/client/src/com/vaadin/terminal/gwt/client/Util.java
new file mode 100644
index 0000000000..a27c77fa45
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/Util.java
@@ -0,0 +1,1181 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Touch;
+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.EventListener;
+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.shared.ComponentState;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+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;
+ }-*/;
+
+ /**
+ *
+ * Returns the topmost element of from given coordinates.
+ *
+ * TODO fix crossplat issues clientX vs pageX. See quircksmode. Not critical
+ * for vaadin as we scroll div istead of page.
+ *
+ * @param x
+ * @param y
+ * @return the element at given coordinates
+ */
+ public static native Element getElementFromPoint(int clientX, int clientY)
+ /*-{
+ var el = $wnd.document.elementFromPoint(clientX, clientY);
+ if(el != null && el.nodeType == 3) {
+ el = el.parentNode;
+ }
+ return el;
+ }-*/;
+
+ /**
+ * 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
+ *
+ * @deprecated since 7.0, use
+ * {@link LayoutManager#setNeedsMeasure(ComponentConnector)}
+ * instead
+ */
+ @Deprecated
+ public static void notifyParentOfSizeChange(Widget widget, boolean lazy) {
+ ComponentConnector connector = findConnectorFor(widget);
+ if (connector != null) {
+ connector.getLayoutManager().setNeedsMeasure(connector);
+ if (!lazy) {
+ connector.getLayoutManager().layoutNow();
+ }
+ }
+ }
+
+ private static ComponentConnector findConnectorFor(Widget widget) {
+ List<ApplicationConnection> runningApplications = ApplicationConfiguration
+ .getRunningApplications();
+ for (ApplicationConnection applicationConnection : runningApplications) {
+ ConnectorMap connectorMap = applicationConnection.getConnectorMap();
+ ComponentConnector connector = connectorMap.getConnector(widget);
+ if (connector == null) {
+ continue;
+ }
+ if (connector.getConnection() == applicationConnection) {
+ return connector;
+ }
+ }
+
+ return null;
+ }
+
+ 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) {
+ VConsole.log("Unable to parse relative size");
+ return -1;
+ }
+ }
+
+ 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);
+ String escapedText = DOM.getInnerHTML(escapeHtmlHelper);
+ if (BrowserInfo.get().isIE8()) {
+ // #7478 IE8 "incorrectly" returns "<br>" for newlines set using
+ // setInnerText. The same for " " which is converted to "&nbsp;"
+ escapedText = escapedText.replaceAll("<(BR|br)>", "\n");
+ escapedText = escapedText.replaceAll("&nbsp;", " ");
+ }
+ return escapedText;
+ }
+
+ /**
+ * Escapes the string so it is safe to write inside an HTML attribute.
+ *
+ * @param attribute
+ * The string to escape
+ * @return An escaped version of <literal>attribute</literal>.
+ */
+ public static String escapeAttribute(String attribute) {
+ attribute = attribute.replace("\"", "&quot;");
+ attribute = attribute.replace("'", "&#39;");
+ attribute = attribute.replace(">", "&gt;");
+ attribute = attribute.replace("<", "&lt;");
+ attribute = attribute.replace("&", "&amp;");
+ return attribute;
+ }
+
+ /**
+ * 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 (offsetHeight < 1) {
+ offsetHeight = 1;
+ }
+ if (offsetWidth < 1) {
+ offsetWidth = 10;
+ }
+ element.getStyle().setPropertyPx("height", offsetHeight);
+ element.getStyle().setPropertyPx("width", offsetWidth);
+
+ borders = element.getOffsetWidth() - element.getClientWidth();
+
+ element.getStyle().setProperty("width", width);
+ 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 (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);
+ 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");
+
+ RootPanel.getBodyElement().removeChild(scroller);
+ }
+ return detectedScrollbarSize;
+ }
+
+ /**
+ * Run workaround for webkits overflow auto issue.
+ *
+ * See: our bug #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 lands sometime to Webkit
+ // Starting from Opera 11.00, also a problem in Opera
+ if (BrowserInfo.get().requiresOverflowAutoFix()) {
+ final String originalOverflow = elem.getStyle().getProperty(
+ "overflow");
+ if ("hidden".equals(originalOverflow)) {
+ return;
+ }
+
+ // check the scrolltop value before hiding the element
+ final int scrolltop = elem.getScrollTop();
+ final int scrollleft = elem.getScrollLeft();
+ elem.getStyle().setProperty("overflow", "hidden");
+
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ // Dough, Safari scroll auto means actually just a moped
+ elem.getStyle().setProperty("overflow", originalOverflow);
+
+ if (scrolltop > 0 || elem.getScrollTop() > 0) {
+ int scrollvalue = scrolltop;
+ if (scrollvalue == 0) {
+ // mysterious are the ways of webkits scrollbar
+ // handling. In some cases webkit reports bad (0)
+ // scrolltop before hiding the element temporary,
+ // sometimes after.
+ scrollvalue = elem.getScrollTop();
+ }
+ // fix another bug where scrollbar remains in wrong
+ // position
+ elem.setScrollTop(scrollvalue - 1);
+ elem.setScrollTop(scrollvalue);
+ }
+
+ // fix for #6940 : Table horizontal scroll sometimes not
+ // updated when collapsing/expanding columns
+ // Also appeared in Safari 5.1 with webkit 534 (#7667)
+ if ((BrowserInfo.get().isChrome() || (BrowserInfo.get()
+ .isSafari() && BrowserInfo.get().getWebkitVersion() >= 534))
+ && (scrollleft > 0 || elem.getScrollLeft() > 0)) {
+ int scrollvalue = scrollleft;
+
+ if (scrollvalue == 0) {
+ // mysterious are the ways of webkits scrollbar
+ // handling. In some cases webkit may report a bad
+ // (0) scrollleft before hiding the element
+ // temporary, sometimes after.
+ scrollvalue = elem.getScrollLeft();
+ }
+ // fix another bug where scrollbar remains in wrong
+ // position
+ elem.setScrollLeft(scrollvalue - 1);
+ elem.setScrollLeft(scrollvalue);
+ }
+ }
+ });
+ }
+
+ }
+
+ /**
+ * Parses shared state and fetches the relative size of the component. If a
+ * dimension is not specified as relative it will return -1. If the shared
+ * state does not contain width or height specifications this will return
+ * null.
+ *
+ * @param state
+ * @return
+ */
+ public static FloatSize parseRelativeSize(ComponentState state) {
+ if (state.isUndefinedHeight() && state.isUndefinedWidth()) {
+ return null;
+ }
+
+ float relativeWidth = Util.parseRelativeSize(state.getWidth());
+ float relativeHeight = Util.parseRelativeSize(state.getHeight());
+
+ FloatSize relativeSize = new FloatSize(relativeWidth, relativeHeight);
+ return relativeSize;
+
+ }
+
+ @Deprecated
+ 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, Widget widget) {
+ notifyParentOfSizeChange(widget, false);
+ }
+
+ public static native int getRequiredWidth(
+ com.google.gwt.dom.client.Element element)
+ /*-{
+ if (element.getBoundingClientRect) {
+ var rect = element.getBoundingClientRect();
+ return Math.ceil(rect.right - rect.left);
+ } else {
+ return element.offsetWidth;
+ }
+ }-*/;
+
+ 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 "";
+ }
+
+ }-*/;
+
+ /**
+ * Locates the nested child component of <literal>parent</literal> which
+ * contains the element <literal>element</literal>. The child component is
+ * also returned if "element" is part of its caption. If
+ * <literal>element</literal> is not part of any child component, null is
+ * returned.
+ *
+ * This method returns the deepest nested VPaintableWidget.
+ *
+ * @param client
+ * A reference to ApplicationConnection
+ * @param parent
+ * The widget that contains <literal>element</literal>.
+ * @param element
+ * An element that is a sub element of the parent
+ * @return The VPaintableWidget which the element is a part of. Null if the
+ * element does not belong to a child.
+ */
+ public static ComponentConnector getConnectorForElement(
+ ApplicationConnection client, Widget parent, Element element) {
+
+ Element browseElement = element;
+ Element rootElement = parent.getElement();
+
+ while (browseElement != null && browseElement != rootElement) {
+
+ ComponentConnector connector = ConnectorMap.get(client)
+ .getConnector(browseElement);
+
+ if (connector == null) {
+ String ownerPid = VCaption.getCaptionOwnerPid(browseElement);
+ if (ownerPid != null) {
+ connector = (ComponentConnector) ConnectorMap.get(client)
+ .getConnector(ownerPid);
+ }
+ }
+
+ if (connector != null) {
+ // check that inside the rootElement
+ while (browseElement != null && browseElement != rootElement) {
+ browseElement = (Element) browseElement.getParentElement();
+ }
+ if (browseElement != rootElement) {
+ return null;
+ } else {
+ return connector;
+ }
+ }
+
+ browseElement = (Element) browseElement.getParentElement();
+ }
+
+ // No connector found, element is possibly inside a VOverlay
+ // If the overlay has an owner, try to find the owner's connector
+ VOverlay overlay = findWidget(element, VOverlay.class);
+ if (overlay != null && overlay.getOwner() != null) {
+ return getConnectorForElement(client, RootPanel.get(), overlay
+ .getOwner().getElement());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Will (attempt) to focus the given DOM Element.
+ *
+ * @param el
+ * the element to focus
+ */
+ public static native void focus(Element el)
+ /*-{
+ try {
+ el.focus();
+ } catch (e) {
+
+ }
+ }-*/;
+
+ /**
+ * Helper method to find the nearest parent paintable instance by traversing
+ * the DOM upwards from given element.
+ *
+ * @param element
+ * the element to start from
+ */
+ public static ComponentConnector findPaintable(
+ ApplicationConnection client, Element element) {
+ Widget widget = Util.findWidget(element, null);
+ ConnectorMap vPaintableMap = ConnectorMap.get(client);
+ while (widget != null && !vPaintableMap.isConnector(widget)) {
+ widget = widget.getParent();
+ }
+ return vPaintableMap.getConnector(widget);
+
+ }
+
+ /**
+ * Helper method to find first instance of given Widget type found by
+ * traversing DOM upwards from given element.
+ *
+ * @param element
+ * the element where to start seeking of Widget
+ * @param class1
+ * the Widget type to seek for
+ */
+ public static <T> T findWidget(Element element,
+ Class<? extends Widget> class1) {
+ if (element != null) {
+ /* First seek for the first EventListener (~Widget) from dom */
+ EventListener eventListener = null;
+ while (eventListener == null && element != null) {
+ eventListener = Event.getEventListener(element);
+ if (eventListener == null) {
+ element = (Element) element.getParentElement();
+ }
+ }
+ if (eventListener != null) {
+ /*
+ * Then find the first widget of type class1 from widget
+ * hierarchy
+ */
+ Widget w = (Widget) eventListener;
+ while (w != null) {
+ if (class1 == null || w.getClass() == class1) {
+ return (T) w;
+ }
+ w = w.getParent();
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Force webkit to redraw an element
+ *
+ * @param element
+ * The element that should be redrawn
+ */
+ public static void forceWebkitRedraw(Element element) {
+ Style style = element.getStyle();
+ String s = style.getProperty("webkitTransform");
+ if (s == null || s.length() == 0) {
+ style.setProperty("webkitTransform", "scale(1)");
+ } else {
+ style.setProperty("webkitTransform", "");
+ }
+ }
+
+ /**
+ * Detaches and re-attaches the element from its parent. The element is
+ * reattached at the same position in the DOM as it was before.
+ *
+ * Does nothing if the element is not attached to the DOM.
+ *
+ * @param element
+ * The element to detach and re-attach
+ */
+ public static void detachAttach(Element element) {
+ if (element == null) {
+ return;
+ }
+
+ Node nextSibling = element.getNextSibling();
+ Node parent = element.getParentNode();
+ if (parent == null) {
+ return;
+ }
+
+ parent.removeChild(element);
+ if (nextSibling == null) {
+ parent.appendChild(element);
+ } else {
+ parent.insertBefore(element, nextSibling);
+ }
+
+ }
+
+ public static void sinkOnloadForImages(Element element) {
+ NodeList<com.google.gwt.dom.client.Element> imgElements = element
+ .getElementsByTagName("img");
+ for (int i = 0; i < imgElements.getLength(); i++) {
+ DOM.sinkEvents((Element) imgElements.getItem(i), Event.ONLOAD);
+ }
+
+ }
+
+ /**
+ * Returns the index of the childElement within its parent.
+ *
+ * @param subElement
+ * @return
+ */
+ public static int getChildElementIndex(Element childElement) {
+ int idx = 0;
+ Node n = childElement;
+ while ((n = n.getPreviousSibling()) != null) {
+ idx++;
+ }
+
+ return idx;
+ }
+
+ private static void printConnectorInvocations(
+ ArrayList<MethodInvocation> invocations, String id,
+ ApplicationConnection c) {
+ ServerConnector connector = ConnectorMap.get(c).getConnector(id);
+ if (connector != null) {
+ VConsole.log("\t" + id + " (" + connector.getClass() + ") :");
+ } else {
+ VConsole.log("\t" + id
+ + ": Warning: no corresponding connector for id " + id);
+ }
+ for (MethodInvocation invocation : invocations) {
+ Object[] parameters = invocation.getParameters();
+ String formattedParams = null;
+ if (ApplicationConnection.UPDATE_VARIABLE_METHOD.equals(invocation
+ .getMethodName()) && parameters.length == 2) {
+ // name, value
+ Object value = parameters[1];
+ // TODO paintables inside lists/maps get rendered as
+ // components in the debug console
+ String formattedValue = value instanceof ServerConnector ? ((ServerConnector) value)
+ .getConnectorId() : String.valueOf(value);
+ formattedParams = parameters[0] + " : " + formattedValue;
+ }
+ if (null == formattedParams) {
+ formattedParams = (null != parameters) ? Arrays
+ .toString(parameters) : null;
+ }
+ VConsole.log("\t\t" + invocation.getInterfaceName() + "."
+ + invocation.getMethodName() + "(" + formattedParams + ")");
+ }
+ }
+
+ static void logVariableBurst(ApplicationConnection c,
+ ArrayList<MethodInvocation> loggedBurst) {
+ try {
+ VConsole.log("Variable burst to be sent to server:");
+ String curId = null;
+ ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>();
+ for (int i = 0; i < loggedBurst.size(); i++) {
+ String id = loggedBurst.get(i).getConnectorId();
+
+ if (curId == null) {
+ curId = id;
+ } else if (!curId.equals(id)) {
+ printConnectorInvocations(invocations, curId, c);
+ invocations.clear();
+ curId = id;
+ }
+ invocations.add(loggedBurst.get(i));
+ }
+ if (!invocations.isEmpty()) {
+ printConnectorInvocations(invocations, curId, c);
+ }
+ } catch (Exception e) {
+ VConsole.error(e);
+ }
+ }
+
+ /**
+ * Temporarily sets the {@code styleProperty} to {@code tempValue} and then
+ * resets it to its current value. Used mainly to work around rendering
+ * issues in IE (and possibly in other browsers)
+ *
+ * @param element
+ * The target element
+ * @param styleProperty
+ * The name of the property to set
+ * @param tempValue
+ * The temporary value
+ */
+ public static void setStyleTemporarily(Element element,
+ final String styleProperty, String tempValue) {
+ final Style style = element.getStyle();
+ final String currentValue = style.getProperty(styleProperty);
+
+ style.setProperty(styleProperty, tempValue);
+ element.getOffsetWidth();
+ style.setProperty(styleProperty, currentValue);
+
+ }
+
+ /**
+ * A helper method to return the client position from an event. Returns
+ * position from either first changed touch (if touch event) or from the
+ * event itself.
+ *
+ * @param event
+ * @return
+ */
+ public static int getTouchOrMouseClientX(Event event) {
+ if (isTouchEvent(event)) {
+ return event.getChangedTouches().get(0).getClientX();
+ } else {
+ return event.getClientX();
+ }
+ }
+
+ /**
+ * Find the element corresponding to the coordinates in the passed mouse
+ * event. Please note that this is not always the same as the target of the
+ * event e.g. if event capture is used.
+ *
+ * @param event
+ * the mouse event to get coordinates from
+ * @return the element at the coordinates of the event
+ */
+ public static Element getElementUnderMouse(NativeEvent event) {
+ int pageX = getTouchOrMouseClientX(event);
+ int pageY = getTouchOrMouseClientY(event);
+
+ return getElementFromPoint(pageX, pageY);
+ }
+
+ /**
+ * A helper method to return the client position from an event. Returns
+ * position from either first changed touch (if touch event) or from the
+ * event itself.
+ *
+ * @param event
+ * @return
+ */
+ public static int getTouchOrMouseClientY(Event event) {
+ if (isTouchEvent(event)) {
+ return event.getChangedTouches().get(0).getClientY();
+ } else {
+ return event.getClientY();
+ }
+ }
+
+ /**
+ *
+ * @see #getTouchOrMouseClientY(Event)
+ * @param currentGwtEvent
+ * @return
+ */
+ public static int getTouchOrMouseClientY(NativeEvent currentGwtEvent) {
+ return getTouchOrMouseClientY(Event.as(currentGwtEvent));
+ }
+
+ /**
+ * @see #getTouchOrMouseClientX(Event)
+ *
+ * @param event
+ * @return
+ */
+ public static int getTouchOrMouseClientX(NativeEvent event) {
+ return getTouchOrMouseClientX(Event.as(event));
+ }
+
+ public static boolean isTouchEvent(Event event) {
+ return event.getType().contains("touch");
+ }
+
+ public static boolean isTouchEvent(NativeEvent event) {
+ return isTouchEvent(Event.as(event));
+ }
+
+ public static void simulateClickFromTouchEvent(Event touchevent,
+ Widget widget) {
+ Touch touch = touchevent.getChangedTouches().get(0);
+ final NativeEvent createMouseUpEvent = Document.get()
+ .createMouseUpEvent(0, touch.getScreenX(), touch.getScreenY(),
+ touch.getClientX(), touch.getClientY(), false, false,
+ false, false, NativeEvent.BUTTON_LEFT);
+ final NativeEvent createMouseDownEvent = Document.get()
+ .createMouseDownEvent(0, touch.getScreenX(),
+ touch.getScreenY(), touch.getClientX(),
+ touch.getClientY(), false, false, false, false,
+ NativeEvent.BUTTON_LEFT);
+ final NativeEvent createMouseClickEvent = Document.get()
+ .createClickEvent(0, touch.getScreenX(), touch.getScreenY(),
+ touch.getClientX(), touch.getClientY(), false, false,
+ false, false);
+
+ /*
+ * Get target with element from point as we want the actual element, not
+ * the one that sunk the event.
+ */
+ final Element target = getElementFromPoint(touch.getClientX(),
+ touch.getClientY());
+
+ /*
+ * Fixes infocusable form fields in Safari of iOS 5.x and some Android
+ * browsers.
+ */
+ Widget targetWidget = findWidget(target, null);
+ if (targetWidget instanceof com.google.gwt.user.client.ui.Focusable) {
+ final com.google.gwt.user.client.ui.Focusable toBeFocusedWidget = (com.google.gwt.user.client.ui.Focusable) targetWidget;
+ toBeFocusedWidget.setFocus(true);
+ } else if (targetWidget instanceof Focusable) {
+ ((Focusable) targetWidget).focus();
+ }
+
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ try {
+ target.dispatchEvent(createMouseDownEvent);
+ target.dispatchEvent(createMouseUpEvent);
+ target.dispatchEvent(createMouseClickEvent);
+ } catch (Exception e) {
+ }
+
+ }
+ });
+
+ }
+
+ /**
+ * Gets the currently focused element for Internet Explorer.
+ *
+ * @return The currently focused element
+ */
+ public native static Element getIEFocusedElement()
+ /*-{
+ if ($wnd.document.activeElement) {
+ return $wnd.document.activeElement;
+ }
+
+ return null;
+ }-*/
+ ;
+
+ /**
+ * Kind of stronger version of isAttached(). In addition to std isAttached,
+ * this method checks that this widget nor any of its parents is hidden. Can
+ * be e.g used to check whether component should react to some events or
+ * not.
+ *
+ * @param widget
+ * @return true if attached and displayed
+ */
+ public static boolean isAttachedAndDisplayed(Widget widget) {
+ if (widget.isAttached()) {
+ /*
+ * Failfast using offset size, then by iterating the widget tree
+ */
+ boolean notZeroSized = widget.getOffsetHeight() > 0
+ || widget.getOffsetWidth() > 0;
+ return notZeroSized || checkVisibilityRecursively(widget);
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean checkVisibilityRecursively(Widget widget) {
+ if (widget.isVisible()) {
+ Widget parent = widget.getParent();
+ if (parent == null) {
+ return true; // root panel
+ } else {
+ return checkVisibilityRecursively(parent);
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Scrolls an element into view vertically only. Modified version of
+ * Element.scrollIntoView.
+ *
+ * @param elem
+ * The element to scroll into view
+ */
+ public static native void scrollIntoViewVertically(Element elem)
+ /*-{
+ var top = elem.offsetTop;
+ var height = elem.offsetHeight;
+
+ if (elem.parentNode != elem.offsetParent) {
+ top -= elem.parentNode.offsetTop;
+ }
+
+ var cur = elem.parentNode;
+ while (cur && (cur.nodeType == 1)) {
+ if (top < cur.scrollTop) {
+ cur.scrollTop = top;
+ }
+ if (top + height > cur.scrollTop + cur.clientHeight) {
+ cur.scrollTop = (top + height) - cur.clientHeight;
+ }
+
+ var offsetTop = cur.offsetTop;
+ if (cur.parentNode != cur.offsetParent) {
+ offsetTop -= cur.parentNode.offsetTop;
+ }
+
+ top += offsetTop - cur.scrollTop;
+ cur = cur.parentNode;
+ }
+ }-*/;
+
+ /**
+ * Checks if the given event is either a touch event or caused by the left
+ * mouse button
+ *
+ * @param event
+ * @return true if the event is a touch event or caused by the left mouse
+ * button, false otherwise
+ */
+ public static boolean isTouchEventOrLeftMouseButton(Event event) {
+ boolean touchEvent = Util.isTouchEvent(event);
+ return touchEvent || event.getButton() == Event.BUTTON_LEFT;
+ }
+
+ /**
+ * Performs a shallow comparison of the collections.
+ *
+ * @param collection1
+ * The first collection
+ * @param collection2
+ * The second collection
+ * @return true if the collections contain the same elements in the same
+ * order, false otherwise
+ */
+ public static boolean collectionsEquals(Collection collection1,
+ Collection collection2) {
+ if (collection1 == null) {
+ return collection2 == null;
+ }
+ if (collection2 == null) {
+ return false;
+ }
+ Iterator<Object> collection1Iterator = collection1.iterator();
+ Iterator<Object> collection2Iterator = collection2.iterator();
+
+ while (collection1Iterator.hasNext()) {
+ if (!collection2Iterator.hasNext()) {
+ return false;
+ }
+ Object collection1Object = collection1Iterator.next();
+ Object collection2Object = collection2Iterator.next();
+ if (collection1Object != collection2Object) {
+ return false;
+ }
+ }
+ if (collection2Iterator.hasNext()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static String getConnectorString(ServerConnector p) {
+ if (p == null) {
+ return "null";
+ }
+ return getSimpleName(p) + " (" + p.getConnectorId() + ")";
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VCaption.java b/client/src/com/vaadin/terminal/gwt/client/VCaption.java
new file mode 100644
index 0000000000..85acc215b7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VCaption.java
@@ -0,0 +1,595 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.shared.AbstractFieldState;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+
+public class VCaption extends HTML {
+
+ public static final String CLASSNAME = "v-caption";
+
+ private final ComponentConnector owner;
+
+ private Element errorIndicatorElement;
+
+ private Element requiredFieldIndicator;
+
+ private Icon icon;
+
+ private Element captionText;
+
+ private final ApplicationConnection client;
+
+ private boolean placedAfterComponent = false;
+
+ private int maxWidth = -1;
+
+ private enum InsertPosition {
+ ICON, CAPTION, REQUIRED, ERROR
+ }
+
+ private TooltipInfo tooltipInfo = null;
+
+ /**
+ * Creates a caption that is not linked to a {@link ComponentConnector}.
+ *
+ * When using this constructor, {@link #getOwner()} returns null.
+ *
+ * @param client
+ * ApplicationConnection
+ * @deprecated all captions should be associated with a paintable widget and
+ * be updated from shared state, not UIDL
+ */
+ @Deprecated
+ public VCaption(ApplicationConnection client) {
+ super();
+ this.client = client;
+ owner = null;
+
+ setStyleName(CLASSNAME);
+ sinkEvents(VTooltip.TOOLTIP_EVENTS);
+
+ }
+
+ /**
+ * Creates a caption for a {@link ComponentConnector}.
+ *
+ * @param component
+ * owner of caption, not null
+ * @param client
+ * ApplicationConnection
+ */
+ public VCaption(ComponentConnector component, ApplicationConnection client) {
+ super();
+ this.client = client;
+ owner = component;
+
+ if (client != null && owner != null) {
+ setOwnerPid(getElement(), owner.getConnectorId());
+ }
+
+ setStyleName(CLASSNAME);
+ }
+
+ /**
+ * Updates the caption from UIDL.
+ *
+ * This method may only be called when the caption has an owner - otherwise,
+ * use {@link #updateCaptionWithoutOwner(UIDL, String, boolean, boolean)}.
+ *
+ * @return true if the position where the caption should be placed has
+ * changed
+ */
+ public boolean updateCaption() {
+ boolean wasPlacedAfterComponent = placedAfterComponent;
+
+ // Caption is placed after component unless there is some part which
+ // moves it above.
+ placedAfterComponent = true;
+
+ String style = CLASSNAME;
+ if (owner.getState().hasStyles()) {
+ for (String customStyle : owner.getState().getStyles()) {
+ style += " " + CLASSNAME + "-" + customStyle;
+ }
+ }
+ if (!owner.isEnabled()) {
+ style += " " + ApplicationConnection.DISABLED_CLASSNAME;
+ }
+ setStyleName(style);
+
+ boolean hasIcon = owner.getState().getIcon() != null;
+ boolean showRequired = false;
+ boolean showError = owner.getState().getErrorMessage() != null;
+ if (owner.getState() instanceof AbstractFieldState) {
+ AbstractFieldState abstractFieldState = (AbstractFieldState) owner
+ .getState();
+ showError = showError && !abstractFieldState.isHideErrors();
+ }
+ if (owner instanceof AbstractFieldConnector) {
+ showRequired = ((AbstractFieldConnector) owner).isRequired();
+ }
+
+ if (hasIcon) {
+ if (icon == null) {
+ icon = new Icon(client);
+ icon.setWidth("0");
+ icon.setHeight("0");
+
+ DOM.insertChild(getElement(), icon.getElement(),
+ getInsertPosition(InsertPosition.ICON));
+ }
+ // Icon forces the caption to be above the component
+ placedAfterComponent = false;
+
+ icon.setUri(owner.getState().getIcon().getURL());
+
+ } else if (icon != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), icon.getElement());
+ icon = null;
+ }
+
+ if (owner.getState().getCaption() != null) {
+ // A caption text should be shown if the attribute is set
+ // If the caption is null the ATTRIBUTE_CAPTION should not be set to
+ // avoid ending up here.
+
+ if (captionText == null) {
+ captionText = DOM.createDiv();
+ captionText.setClassName("v-captiontext");
+
+ DOM.insertChild(getElement(), captionText,
+ getInsertPosition(InsertPosition.CAPTION));
+ }
+
+ // Update caption text
+ String c = owner.getState().getCaption();
+ // A text forces the caption to be above the component.
+ placedAfterComponent = false;
+ if (c == null || c.trim().equals("")) {
+ // Not sure if c even can be null. Should not.
+
+ // This is required to ensure that the caption uses space in all
+ // browsers when it is set to the empty string. If there is an
+ // icon, error indicator or required indicator they will ensure
+ // that space is reserved.
+ if (!hasIcon && !showRequired && !showError) {
+ captionText.setInnerHTML("&nbsp;");
+ }
+ } else {
+ DOM.setInnerText(captionText, c);
+ }
+
+ } else if (captionText != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), captionText);
+ captionText = null;
+ }
+
+ if (owner.getState().hasDescription() && captionText != null) {
+ addStyleDependentName("hasdescription");
+ } else {
+ removeStyleDependentName("hasdescription");
+ }
+
+ if (showRequired) {
+ if (requiredFieldIndicator == null) {
+ requiredFieldIndicator = DOM.createDiv();
+ requiredFieldIndicator
+ .setClassName("v-required-field-indicator");
+ DOM.setInnerText(requiredFieldIndicator, "*");
+
+ DOM.insertChild(getElement(), requiredFieldIndicator,
+ getInsertPosition(InsertPosition.REQUIRED));
+ }
+ } else if (requiredFieldIndicator != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), requiredFieldIndicator);
+ requiredFieldIndicator = null;
+ }
+
+ if (showError) {
+ if (errorIndicatorElement == null) {
+ errorIndicatorElement = DOM.createDiv();
+ DOM.setInnerHTML(errorIndicatorElement, "&nbsp;");
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "v-errorindicator");
+
+ DOM.insertChild(getElement(), errorIndicatorElement,
+ getInsertPosition(InsertPosition.ERROR));
+ }
+ } else if (errorIndicatorElement != null) {
+ // Remove existing
+ getElement().removeChild(errorIndicatorElement);
+ errorIndicatorElement = null;
+ }
+
+ return (wasPlacedAfterComponent != placedAfterComponent);
+ }
+
+ private int getInsertPosition(InsertPosition element) {
+ int pos = 0;
+ if (InsertPosition.ICON.equals(element)) {
+ return pos;
+ }
+ if (icon != null) {
+ pos++;
+ }
+
+ if (InsertPosition.CAPTION.equals(element)) {
+ return pos;
+ }
+
+ if (captionText != null) {
+ pos++;
+ }
+
+ if (InsertPosition.REQUIRED.equals(element)) {
+ return pos;
+ }
+ if (requiredFieldIndicator != null) {
+ pos++;
+ }
+
+ // if (InsertPosition.ERROR.equals(element)) {
+ // }
+ return pos;
+
+ }
+
+ @Deprecated
+ public boolean updateCaptionWithoutOwner(String caption, boolean disabled,
+ boolean hasDescription, boolean hasError, String iconURL) {
+ boolean wasPlacedAfterComponent = placedAfterComponent;
+
+ // Caption is placed after component unless there is some part which
+ // moves it above.
+ placedAfterComponent = true;
+
+ String style = VCaption.CLASSNAME;
+ if (disabled) {
+ style += " " + ApplicationConnection.DISABLED_CLASSNAME;
+ }
+ setStyleName(style);
+ if (hasDescription) {
+ if (captionText != null) {
+ addStyleDependentName("hasdescription");
+ } else {
+ removeStyleDependentName("hasdescription");
+ }
+ }
+ boolean hasIcon = iconURL != null;
+
+ if (hasIcon) {
+ if (icon == null) {
+ icon = new Icon(client);
+ icon.setWidth("0");
+ icon.setHeight("0");
+
+ DOM.insertChild(getElement(), icon.getElement(),
+ getInsertPosition(InsertPosition.ICON));
+ }
+ // Icon forces the caption to be above the component
+ placedAfterComponent = false;
+
+ icon.setUri(iconURL);
+
+ } else if (icon != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), icon.getElement());
+ icon = null;
+ }
+
+ if (caption != null) {
+ // A caption text should be shown if the attribute is set
+ // If the caption is null the ATTRIBUTE_CAPTION should not be set to
+ // avoid ending up here.
+
+ if (captionText == null) {
+ captionText = DOM.createDiv();
+ captionText.setClassName("v-captiontext");
+
+ DOM.insertChild(getElement(), captionText,
+ getInsertPosition(InsertPosition.CAPTION));
+ }
+
+ // Update caption text
+ // A text forces the caption to be above the component.
+ placedAfterComponent = false;
+ if (caption.trim().equals("")) {
+ // This is required to ensure that the caption uses space in all
+ // browsers when it is set to the empty string. If there is an
+ // icon, error indicator or required indicator they will ensure
+ // that space is reserved.
+ if (!hasIcon && !hasError) {
+ captionText.setInnerHTML("&nbsp;");
+ }
+ } else {
+ DOM.setInnerText(captionText, caption);
+ }
+
+ } else if (captionText != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), captionText);
+ captionText = null;
+ }
+
+ if (hasError) {
+ if (errorIndicatorElement == null) {
+ errorIndicatorElement = DOM.createDiv();
+ DOM.setInnerHTML(errorIndicatorElement, "&nbsp;");
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "v-errorindicator");
+
+ DOM.insertChild(getElement(), errorIndicatorElement,
+ getInsertPosition(InsertPosition.ERROR));
+ }
+ } else if (errorIndicatorElement != null) {
+ // Remove existing
+ getElement().removeChild(errorIndicatorElement);
+ errorIndicatorElement = null;
+ }
+
+ return (wasPlacedAfterComponent != placedAfterComponent);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ final Element target = DOM.eventGetTarget(event);
+
+ if (DOM.eventGetType(event) == Event.ONLOAD
+ && icon.getElement() == target) {
+ icon.setWidth("");
+ icon.setHeight("");
+
+ // 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 VCaption to layouts
+ */
+ if (owner != null) {
+ Util.notifyParentOfSizeChange(owner.getWidget(), true);
+ } else {
+ VConsole.log("Warning: Icon load event was not propagated because VCaption owner is unknown.");
+ }
+ }
+ }
+
+ public static boolean isNeeded(ComponentState state) {
+ if (state.getCaption() != null) {
+ return true;
+ }
+ if (state.getIcon() != null) {
+ return true;
+ }
+ if (state.getErrorMessage() != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns Paintable for which this Caption belongs to.
+ *
+ * @return owner Widget
+ */
+ public ComponentConnector 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().isFirefox()) {
+ /*
+ * 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 = Util.getRequiredHeight(icon.getElement());
+ if (h > height) {
+ height = h;
+ }
+ }
+
+ if (captionText != null) {
+ h = Util.getRequiredHeight(captionText);
+ if (h > height) {
+ height = h;
+ }
+ }
+ if (requiredFieldIndicator != null) {
+ h = Util.getRequiredHeight(requiredFieldIndicator);
+ if (h > height) {
+ height = h;
+ }
+ }
+ if (errorIndicatorElement != null) {
+ h = Util.getRequiredHeight(errorIndicatorElement);
+ 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;
+ }
+
+ }
+
+ }
+ }
+
+ /**
+ * Sets the tooltip that should be shown for the caption
+ *
+ * @param tooltipInfo
+ * The tooltip that should be shown or null if no tooltip should
+ * be shown
+ */
+ public void setTooltipInfo(TooltipInfo tooltipInfo) {
+ this.tooltipInfo = tooltipInfo;
+ }
+
+ /**
+ * Returns the tooltip that should be shown for the caption
+ *
+ * @return The tooltip to show or null if no tooltip should be shown
+ */
+ public TooltipInfo getTooltipInfo() {
+ return tooltipInfo;
+ }
+
+ protected Element getTextElement() {
+ return captionText;
+ }
+
+ public static String getCaptionOwnerPid(Element e) {
+ return getOwnerPid(e);
+ }
+
+ private native static void setOwnerPid(Element el, String pid)
+ /*-{
+ el.vOwnerPid = pid;
+ }-*/;
+
+ public native static String getOwnerPid(Element el)
+ /*-{
+ return el.vOwnerPid;
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java b/client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java
new file mode 100644
index 0000000000..a8dabb8652
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VCaptionWrapper.java
@@ -0,0 +1,39 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class VCaptionWrapper extends FlowPanel {
+
+ public static final String CLASSNAME = "v-captionwrapper";
+ VCaption caption;
+ ComponentConnector wrappedConnector;
+
+ /**
+ * Creates a new caption wrapper panel.
+ *
+ * @param toBeWrapped
+ * paintable that the caption is associated with, not null
+ * @param client
+ * ApplicationConnection
+ */
+ public VCaptionWrapper(ComponentConnector toBeWrapped,
+ ApplicationConnection client) {
+ caption = new VCaption(toBeWrapped, client);
+ add(caption);
+ wrappedConnector = toBeWrapped;
+ add(wrappedConnector.getWidget());
+ setStyleName(CLASSNAME);
+ }
+
+ public void updateCaption() {
+ caption.updateCaption();
+ }
+
+ public ComponentConnector getWrappedConnector() {
+ return wrappedConnector;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VConsole.java b/client/src/com/vaadin/terminal/gwt/client/VConsole.java
new file mode 100644
index 0000000000..02fc61626d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VConsole.java
@@ -0,0 +1,105 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Set;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * A helper class to do some client side logging.
+ * <p>
+ * This class replaces previously used loggin style:
+ * ApplicationConnection.getConsole().log("foo").
+ * <p>
+ * The default widgetset provides three modes for debugging:
+ * <ul>
+ * <li>NullConsole (Default, displays no errors at all)
+ * <li>VDebugConsole ( Enabled by appending ?debug to url. Displays a floating
+ * console in the browser and also prints to browsers internal console (builtin
+ * or Firebug) and GWT's development mode console if available.)
+ * <li>VDebugConsole in quiet mode (Enabled by appending ?debug=quiet. Same as
+ * previous but without the console floating over application).
+ * </ul>
+ * <p>
+ * Implementations can be customized with GWT deferred binding by overriding
+ * NullConsole.class or VDebugConsole.class. This way developer can for example
+ * build mechanism to send client side logging data to a server.
+ * <p>
+ * Note that logging in client side is not fully optimized away even in
+ * production mode. Use logging moderately in production code to keep the size
+ * of client side engine small. An exception is {@link GWT#log(String)} style
+ * logging, which is available only in GWT development mode, but optimized away
+ * when compiled to web mode.
+ *
+ *
+ * TODO improve javadocs of individual methods
+ *
+ */
+public class VConsole {
+ private static Console impl;
+
+ /**
+ * Used by ApplicationConfiguration to initialize VConsole.
+ *
+ * @param console
+ */
+ static void setImplementation(Console console) {
+ impl = console;
+ }
+
+ /**
+ * Used by ApplicationConnection to support deprecated getConsole() api.
+ */
+ static Console getImplementation() {
+ return impl;
+ }
+
+ public static void log(String msg) {
+ if (impl != null) {
+ impl.log(msg);
+ }
+ }
+
+ public static void log(Throwable e) {
+ if (impl != null) {
+ impl.log(e);
+ }
+ }
+
+ public static void error(Throwable e) {
+ if (impl != null) {
+ impl.error(e);
+ }
+ }
+
+ public static void error(String msg) {
+ if (impl != null) {
+ impl.error(msg);
+ }
+ }
+
+ public static void printObject(Object msg) {
+ if (impl != null) {
+ impl.printObject(msg);
+ }
+ }
+
+ public static void dirUIDL(ValueMap u, ApplicationConnection client) {
+ if (impl != null) {
+ impl.dirUIDL(u, client);
+ }
+ }
+
+ public static void printLayoutProblems(ValueMap meta,
+ ApplicationConnection applicationConnection,
+ Set<ComponentConnector> zeroHeightComponents,
+ Set<ComponentConnector> zeroWidthComponents) {
+ if (impl != null) {
+ impl.printLayoutProblems(meta, applicationConnection,
+ zeroHeightComponents, zeroWidthComponents);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java b/client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java
new file mode 100644
index 0000000000..5753e032d4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VDebugConsole.java
@@ -0,0 +1,1004 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.FontWeight;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.UmbrellaException;
+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.http.client.UrlBuilder;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Cookies;
+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.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+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.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.RootPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+import com.vaadin.terminal.gwt.client.ui.root.RootConnector;
+import com.vaadin.terminal.gwt.client.ui.window.WindowConnector;
+
+/**
+ * A helper console for client side development. The debug console can also be
+ * used to resolve layout issues, inspect the communication between browser and
+ * the server, start GWT dev mode and restart application.
+ *
+ * <p>
+ * This implementation is used vaadin is in debug mode (see manual) and
+ * developer appends "?debug" query parameter to url. Debug information can also
+ * be shown on browsers internal console only, by appending "?debug=quiet" query
+ * parameter.
+ * <p>
+ * This implementation can be overridden with GWT deferred binding.
+ *
+ */
+public class VDebugConsole extends VOverlay implements Console {
+
+ private final class HighlightModeHandler implements NativePreviewHandler {
+ private final Label label;
+
+ private HighlightModeHandler(Label label) {
+ this.label = label;
+ }
+
+ @Override
+ public void onPreviewNativeEvent(NativePreviewEvent event) {
+ if (event.getTypeInt() == Event.ONKEYDOWN
+ && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+ highlightModeRegistration.removeHandler();
+ VUIDLBrowser.deHiglight();
+ return;
+ }
+ if (event.getTypeInt() == Event.ONMOUSEMOVE) {
+ VUIDLBrowser.deHiglight();
+ Element eventTarget = Util.getElementFromPoint(event
+ .getNativeEvent().getClientX(), event.getNativeEvent()
+ .getClientY());
+ if (getElement().isOrHasChild(eventTarget)) {
+ return;
+ }
+
+ for (ApplicationConnection a : ApplicationConfiguration
+ .getRunningApplications()) {
+ ComponentConnector connector = Util.getConnectorForElement(
+ a, a.getRootConnector().getWidget(), eventTarget);
+ if (connector == null) {
+ connector = Util.getConnectorForElement(a,
+ RootPanel.get(), eventTarget);
+ }
+ if (connector != null) {
+ String pid = connector.getConnectorId();
+ VUIDLBrowser.highlight(connector);
+ label.setText("Currently focused :"
+ + connector.getClass() + " ID:" + pid);
+ event.cancel();
+ event.consume();
+ event.getNativeEvent().stopPropagation();
+ return;
+ }
+ }
+ }
+ if (event.getTypeInt() == Event.ONCLICK) {
+ VUIDLBrowser.deHiglight();
+ event.cancel();
+ event.consume();
+ event.getNativeEvent().stopPropagation();
+ highlightModeRegistration.removeHandler();
+ Element eventTarget = Util.getElementFromPoint(event
+ .getNativeEvent().getClientX(), event.getNativeEvent()
+ .getClientY());
+ for (ApplicationConnection a : ApplicationConfiguration
+ .getRunningApplications()) {
+ ComponentConnector paintable = Util.getConnectorForElement(
+ a, a.getRootConnector().getWidget(), eventTarget);
+ if (paintable == null) {
+ paintable = Util.getConnectorForElement(a,
+ RootPanel.get(), eventTarget);
+ }
+
+ if (paintable != null) {
+ a.highlightComponent(paintable);
+ return;
+ }
+ }
+ }
+ event.cancel();
+ }
+ }
+
+ private static final String POS_COOKIE_NAME = "VDebugConsolePos";
+
+ private HandlerRegistration highlightModeRegistration;
+
+ Element caption = DOM.createDiv();
+
+ private Panel panel;
+
+ private Button clear = new Button("C");
+ private Button restart = new Button("R");
+ private Button forceLayout = new Button("FL");
+ private Button analyzeLayout = new Button("AL");
+ private Button savePosition = new Button("S");
+ private Button highlight = new Button("H");
+ private Button connectorStats = new Button("CS");
+ private CheckBox devMode = new CheckBox("Dev");
+ private CheckBox superDevMode = new CheckBox("SDev");
+ private CheckBox autoScroll = new CheckBox("Autoscroll ");
+ 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 static final String help = "Drag title=move, shift-drag=resize, doubleclick title=min/max."
+ + "Use debug=quiet to log only to browser console.";
+
+ private static final int DEFAULT_WIDTH = 650;
+ private static final int DEFAULT_HEIGHT = 400;
+
+ public VDebugConsole() {
+ super(false, false);
+ getElement().getStyle().setOverflow(Overflow.HIDDEN);
+ clear.setTitle("Clear console");
+ restart.setTitle("Restart app");
+ forceLayout.setTitle("Force layout");
+ analyzeLayout.setTitle("Analyze layouts");
+ savePosition.setTitle("Save pos");
+ }
+
+ private EventPreview dragpreview = new EventPreview() {
+
+ @Override
+ public boolean onEventPreview(Event event) {
+ onBrowserEvent(event);
+ return false;
+ }
+ };
+
+ private boolean quietMode;
+
+ @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 = VDebugConsole.this.getOffsetWidth();
+ initialH = VDebugConsole.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;
+ }
+ VDebugConsole.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;
+ }
+ VDebugConsole.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);
+ setToDefaultSizeAndPos();
+ } else {
+ panel.setVisible(false);
+ setPixelSize(120, 20);
+ setPopupPosition(Window.getClientWidth() - 125,
+ Window.getClientHeight() - 25);
+ }
+ collapsed = !collapsed;
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ private void setToDefaultSizeAndPos() {
+ String cookie = Cookies.getCookie(POS_COOKIE_NAME);
+ int width, height, top, left;
+ boolean autoScrollValue = false;
+ if (cookie != null) {
+ String[] split = cookie.split(",");
+ left = Integer.parseInt(split[0]);
+ top = Integer.parseInt(split[1]);
+ width = Integer.parseInt(split[2]);
+ height = Integer.parseInt(split[3]);
+ autoScrollValue = Boolean.valueOf(split[4]);
+ } else {
+ int windowHeight = Window.getClientHeight();
+ int windowWidth = Window.getClientWidth();
+ width = DEFAULT_WIDTH;
+ height = DEFAULT_HEIGHT;
+
+ if (height > windowHeight / 2) {
+ height = windowHeight / 2;
+ }
+ if (width > windowWidth / 2) {
+ width = windowWidth / 2;
+ }
+
+ top = windowHeight - (height + 10);
+ left = windowWidth - (width + 10);
+ }
+ setPixelSize(width, height);
+ setPopupPosition(left, top);
+ autoScroll.setValue(autoScrollValue);
+ }
+
+ @Override
+ public void setPixelSize(int width, int height) {
+ if (height < 20) {
+ height = 20;
+ }
+ if (width < 2) {
+ width = 2;
+ }
+ panel.setHeight((height - 20) + "px");
+ panel.setWidth((width - 2) + "px");
+ getElement().getStyle().setWidth(width, Unit.PX);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Console#log(java.lang.String)
+ */
+ @Override
+ public void log(String msg) {
+ if (msg == null) {
+ msg = "null";
+ }
+ msg = addTimestamp(msg);
+ // remoteLog(msg);
+
+ logToDebugWindow(msg, false);
+ GWT.log(msg);
+ consoleLog(msg);
+ System.out.println(msg);
+ }
+
+ private List<String> msgQueue = new LinkedList<String>();
+
+ private ScheduledCommand doSend = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (!msgQueue.isEmpty()) {
+ RequestBuilder requestBuilder = new RequestBuilder(
+ RequestBuilder.POST, getRemoteLogUrl());
+ try {
+ String requestData = "";
+ for (String str : msgQueue) {
+ requestData += str;
+ requestData += "\n";
+ }
+ requestBuilder.sendRequest(requestData,
+ new RequestCallback() {
+
+ @Override
+ public void onResponseReceived(Request request,
+ Response response) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onError(Request request,
+ Throwable exception) {
+ // TODO Auto-generated method stub
+
+ }
+ });
+ } catch (RequestException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ msgQueue.clear();
+ }
+ }
+
+ };
+ private VLazyExecutor sendToRemoteLog = new VLazyExecutor(350, doSend);
+
+ protected String getRemoteLogUrl() {
+ return "http://sun-vehje.local:8080/remotelog/";
+ }
+
+ protected void remoteLog(String msg) {
+ msgQueue.add(msg);
+ sendToRemoteLog.trigger();
+ }
+
+ /**
+ * Logs the given message to the debug window.
+ *
+ * @param msg
+ * The message to log. Must not be null.
+ */
+ private void logToDebugWindow(String msg, boolean error) {
+ Widget row;
+ if (error) {
+ row = createErrorHtml(msg);
+ } else {
+ row = new HTML(msg);
+ }
+ panel.add(row);
+ if (autoScroll.getValue()) {
+ row.getElement().scrollIntoView();
+ }
+ }
+
+ private HTML createErrorHtml(String msg) {
+ HTML html = new HTML(msg);
+ html.getElement().getStyle().setColor("#f00");
+ html.getElement().getStyle().setFontWeight(FontWeight.BOLD);
+ return html;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Console#error(java.lang.String)
+ */
+ @Override
+ public void error(String msg) {
+ if (msg == null) {
+ msg = "null";
+ }
+ msg = addTimestamp(msg);
+ logToDebugWindow(msg, true);
+
+ GWT.log(msg);
+ consoleErr(msg);
+ System.out.println(msg);
+
+ }
+
+ DateTimeFormat timestampFormat = DateTimeFormat.getFormat("HH:mm:ss:SSS");
+
+ @SuppressWarnings("deprecation")
+ private String addTimestamp(String msg) {
+ Date date = new Date();
+ String timestamp = timestampFormat.format(date);
+ return timestamp + " " + msg;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Console#printObject(java.lang.
+ * Object)
+ */
+ @Override
+ public void printObject(Object msg) {
+ String str;
+ if (msg == null) {
+ str = "null";
+ } else {
+ str = msg.toString();
+ }
+ panel.add((new Label(str)));
+ consoleLog(str);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Console#dirUIDL(com.vaadin
+ * .terminal.gwt.client.UIDL)
+ */
+ @Override
+ public void dirUIDL(ValueMap u, ApplicationConnection client) {
+ if (panel.isAttached()) {
+ VUIDLBrowser vuidlBrowser = new VUIDLBrowser(u, client);
+ vuidlBrowser.setText("Response:");
+ panel.add(vuidlBrowser);
+ }
+ consoleDir(u);
+ // consoleLog(u.getChildrenAsXML());
+ }
+
+ private static native void consoleDir(ValueMap u)
+ /*-{
+ if($wnd.console && $wnd.console.log) {
+ if($wnd.console.dir) {
+ $wnd.console.dir(u);
+ } else {
+ $wnd.console.log(u);
+ }
+ }
+
+ }-*/;
+
+ 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);
+ }
+ }-*/;
+
+ @Override
+ public void printLayoutProblems(ValueMap meta, ApplicationConnection ac,
+ Set<ComponentConnector> zeroHeightComponents,
+ Set<ComponentConnector> zeroWidthComponents) {
+ JsArray<ValueMap> valueMapArray = meta
+ .getJSValueMapArray("invalidLayouts");
+ int size = valueMapArray.length();
+ panel.add(new HTML("<div>************************</di>"
+ + "<h4>Layouts analyzed on server, total top level problems: "
+ + size + " </h4>"));
+ if (size > 0) {
+ SimpleTree root = new SimpleTree("Root problems");
+
+ for (int i = 0; i < size; i++) {
+ printLayoutError(valueMapArray.get(i), root, ac);
+ }
+ panel.add(root);
+
+ }
+ if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) {
+ panel.add(new HTML("<h4> Client side notifications</h4>"
+ + " <em>The following relative sized components were "
+ + "rendered to a zero size container on the client side."
+ + " Note that these are not necessarily invalid "
+ + "states, but 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<ComponentConnector> zeroHeightComponents,
+ ApplicationConnection ac) {
+ for (final ComponentConnector paintable : zeroHeightComponents) {
+ final ServerConnector parent = paintable.getParent();
+
+ VerticalPanel errorDetails = new VerticalPanel();
+ errorDetails.add(new Label("" + Util.getSimpleName(paintable)
+ + " inside " + Util.getSimpleName(parent)));
+ if (parent instanceof ComponentConnector) {
+ ComponentConnector parentComponent = (ComponentConnector) parent;
+ final Widget layout = parentComponent.getWidget();
+
+ final CheckBox emphasisInUi = new CheckBox(
+ "Emphasize components parent in UI (the actual component is not visible)");
+ emphasisInUi.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ Element element2 = layout.getElement();
+ Widget.setStyleName(element2, "invalidlayout",
+ emphasisInUi.getValue().booleanValue());
+ }
+ });
+
+ errorDetails.add(emphasisInUi);
+ }
+ panel.add(errorDetails);
+ }
+ }
+
+ private void printLayoutError(ValueMap valueMap, SimpleTree root,
+ final ApplicationConnection ac) {
+ final String pid = valueMap.getString("id");
+ final ComponentConnector paintable = (ComponentConnector) ConnectorMap
+ .get(ac).getConnector(pid);
+
+ SimpleTree errorNode = new SimpleTree();
+ VerticalPanel errorDetails = new VerticalPanel();
+ errorDetails.add(new Label(Util.getSimpleName(paintable) + " id: "
+ + pid));
+ if (valueMap.containsKey("heightMsg")) {
+ errorDetails.add(new Label("Height problem: "
+ + valueMap.getString("heightMsg")));
+ }
+ if (valueMap.containsKey("widthMsg")) {
+ errorDetails.add(new Label("Width problem: "
+ + valueMap.getString("widthMsg")));
+ }
+ final CheckBox emphasisInUi = new CheckBox("Emphasize component in UI");
+ emphasisInUi.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (paintable != null) {
+ Element element2 = paintable.getWidget().getElement();
+ Widget.setStyleName(element2, "invalidlayout",
+ emphasisInUi.getValue());
+ }
+ }
+ });
+ errorDetails.add(emphasisInUi);
+ errorNode.add(errorDetails);
+ if (valueMap.containsKey("subErrors")) {
+ HTML l = new HTML(
+ "<em>Expand this node to show problems that may be dependent on this problem.</em>");
+ errorDetails.add(l);
+ JsArray<ValueMap> suberrors = valueMap
+ .getJSValueMapArray("subErrors");
+ for (int i = 0; i < suberrors.length(); i++) {
+ ValueMap value = suberrors.get(i);
+ printLayoutError(value, errorNode, ac);
+ }
+
+ }
+ root.add(errorNode);
+ }
+
+ @Override
+ public void log(Throwable e) {
+ if (e instanceof UmbrellaException) {
+ UmbrellaException ue = (UmbrellaException) e;
+ for (Throwable t : ue.getCauses()) {
+ log(t);
+ }
+ return;
+ }
+ log(Util.getSimpleName(e) + ": " + e.getMessage());
+ GWT.log(e.getMessage(), e);
+ }
+
+ @Override
+ public void error(Throwable e) {
+ handleError(e, this);
+ }
+
+ static void handleError(Throwable e, Console target) {
+ if (e instanceof UmbrellaException) {
+ UmbrellaException ue = (UmbrellaException) e;
+ for (Throwable t : ue.getCauses()) {
+ target.error(t);
+ }
+ return;
+ }
+ String exceptionText = Util.getSimpleName(e);
+ String message = e.getMessage();
+ if (message != null && message.length() != 0) {
+ exceptionText += ": " + e.getMessage();
+ }
+ target.error(exceptionText);
+ GWT.log(e.getMessage(), e);
+ if (!GWT.isProdMode()) {
+ e.printStackTrace();
+ }
+ try {
+ VNotification.createNotification(VNotification.DELAY_FOREVER).show(
+ "<h1>Uncaught client side exception</h1><br />"
+ + exceptionText, VNotification.CENTERED, "error");
+ } catch (Exception e2) {
+ // Just swallow this exception
+ }
+ }
+
+ @Override
+ public void init() {
+ panel = new FlowPanel();
+ if (!quietMode) {
+ DOM.appendChild(getContainerElement(), caption);
+ setWidget(panel);
+ caption.setClassName("v-debug-console-caption");
+ setStyleName("v-debug-console");
+ getElement().getStyle().setZIndex(20000);
+ getElement().getStyle().setOverflow(Overflow.HIDDEN);
+
+ sinkEvents(Event.ONDBLCLICK);
+
+ sinkEvents(Event.MOUSEEVENTS);
+
+ panel.setStyleName("v-debug-console-content");
+
+ caption.setInnerHTML("Debug window");
+ caption.getStyle().setHeight(25, Unit.PX);
+ caption.setTitle(help);
+
+ show();
+ setToDefaultSizeAndPos();
+
+ actions = new HorizontalPanel();
+ Style style = actions.getElement().getStyle();
+ style.setPosition(Position.ABSOLUTE);
+ style.setBackgroundColor("#666");
+ style.setLeft(135, Unit.PX);
+ style.setHeight(25, Unit.PX);
+ style.setTop(0, Unit.PX);
+
+ actions.add(clear);
+ actions.add(restart);
+ actions.add(forceLayout);
+ actions.add(analyzeLayout);
+ actions.add(highlight);
+ actions.add(connectorStats);
+ connectorStats.setTitle("Show connector statistics for client");
+ highlight
+ .setTitle("Select a component and print details about it to the server log and client side console.");
+ actions.add(savePosition);
+ savePosition
+ .setTitle("Saves the position and size of debug console to a cookie");
+ actions.add(autoScroll);
+ addDevMode();
+ addSuperDevMode();
+
+ autoScroll
+ .setTitle("Automatically scroll so that new messages are visible");
+
+ panel.add(actions);
+
+ panel.add(new HTML("<i>" + help + "</i>"));
+
+ clear.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ int width = panel.getOffsetWidth();
+ int height = panel.getOffsetHeight();
+ panel = new FlowPanel();
+ panel.setPixelSize(width, height);
+ panel.setStyleName("v-debug-console-content");
+ panel.add(actions);
+ setWidget(panel);
+ }
+ });
+
+ restart.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+
+ 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.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ for (ApplicationConnection applicationConnection : ApplicationConfiguration
+ .getRunningApplications()) {
+ applicationConnection.forceLayout();
+ }
+ }
+ });
+
+ analyzeLayout.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ 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 and loss of"
+ + " all non committed variables form client side.");
+
+ savePosition.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ String pos = getAbsoluteLeft() + "," + getAbsoluteTop()
+ + "," + getOffsetWidth() + "," + getOffsetHeight()
+ + "," + autoScroll.getValue();
+ Cookies.setCookie(POS_COOKIE_NAME, pos);
+ }
+ });
+
+ highlight.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ final Label label = new Label("--");
+ log("<i>Use mouse to select a component or click ESC to exit highlight mode.</i>");
+ panel.add(label);
+ highlightModeRegistration = Event
+ .addNativePreviewHandler(new HighlightModeHandler(
+ label));
+
+ }
+ });
+
+ }
+ connectorStats.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ for (ApplicationConnection a : ApplicationConfiguration
+ .getRunningApplications()) {
+ dumpConnectorInfo(a);
+ }
+ }
+ });
+ log("Starting Vaadin client side engine. Widgetset: "
+ + GWT.getModuleName());
+
+ log("Widget set is built on version: "
+ + ApplicationConfiguration.VERSION);
+
+ logToDebugWindow("<div class=\"v-theme-version v-theme-version-"
+ + ApplicationConfiguration.VERSION.replaceAll("\\.", "_")
+ + "\">Warning: widgetset version "
+ + ApplicationConfiguration.VERSION
+ + " does not seem to match theme version </div>", true);
+
+ }
+
+ private void addSuperDevMode() {
+ final Storage sessionStorage = Storage.getSessionStorageIfSupported();
+ if (sessionStorage == null) {
+ return;
+ }
+ actions.add(superDevMode);
+ if (Location.getParameter("superdevmode") != null) {
+ superDevMode.setValue(true);
+ }
+ superDevMode.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ SuperDevMode.redirect(event.getValue());
+ }
+
+ });
+
+ }
+
+ private void addDevMode() {
+ actions.add(devMode);
+ if (Location.getParameter("gwt.codesvr") != null) {
+ devMode.setValue(true);
+ }
+ devMode.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (devMode.getValue()) {
+ addHMParameter();
+ } else {
+ removeHMParameter();
+ }
+ }
+
+ private void addHMParameter() {
+ UrlBuilder createUrlBuilder = Location.createUrlBuilder();
+ createUrlBuilder.setParameter("gwt.codesvr", "localhost:9997");
+ Location.assign(createUrlBuilder.buildString());
+ }
+
+ private void removeHMParameter() {
+ UrlBuilder createUrlBuilder = Location.createUrlBuilder();
+ createUrlBuilder.removeParameter("gwt.codesvr");
+ Location.assign(createUrlBuilder.buildString());
+
+ }
+ });
+ }
+
+ protected void dumpConnectorInfo(ApplicationConnection a) {
+ RootConnector root = a.getRootConnector();
+ log("================");
+ log("Connector hierarchy for Root: " + root.getState().getCaption()
+ + " (" + root.getConnectorId() + ")");
+ Set<ServerConnector> connectorsInHierarchy = new HashSet<ServerConnector>();
+ SimpleTree rootHierachy = dumpConnectorHierarchy(root, "",
+ connectorsInHierarchy);
+ if (panel.isAttached()) {
+ rootHierachy.open(true);
+ panel.add(rootHierachy);
+ }
+
+ ConnectorMap connectorMap = a.getConnectorMap();
+ Collection<? extends ServerConnector> registeredConnectors = connectorMap
+ .getConnectors();
+ log("Sub windows:");
+ Set<ServerConnector> subWindowHierarchyConnectors = new HashSet<ServerConnector>();
+ for (WindowConnector wc : root.getSubWindows()) {
+ SimpleTree windowHierachy = dumpConnectorHierarchy(wc, "",
+ subWindowHierarchyConnectors);
+ if (panel.isAttached()) {
+ windowHierachy.open(true);
+ panel.add(windowHierachy);
+ }
+ }
+ log("Registered connectors not in hierarchy (should be empty):");
+ for (ServerConnector registeredConnector : registeredConnectors) {
+
+ if (connectorsInHierarchy.contains(registeredConnector)) {
+ continue;
+ }
+
+ if (subWindowHierarchyConnectors.contains(registeredConnector)) {
+ continue;
+ }
+ error(getConnectorString(registeredConnector));
+
+ }
+ log("Unregistered connectors in hierarchy (should be empty):");
+ for (ServerConnector hierarchyConnector : connectorsInHierarchy) {
+ if (!connectorMap.hasConnector(hierarchyConnector.getConnectorId())) {
+ error(getConnectorString(hierarchyConnector));
+ }
+
+ }
+
+ log("================");
+
+ }
+
+ private SimpleTree dumpConnectorHierarchy(final ServerConnector connector,
+ String indent, Set<ServerConnector> connectors) {
+ SimpleTree simpleTree = new SimpleTree(getConnectorString(connector)) {
+ @Override
+ protected void select(ClickEvent event) {
+ super.select(event);
+ if (connector instanceof ComponentConnector) {
+ VUIDLBrowser.highlight((ComponentConnector) connector);
+ }
+ }
+ };
+ simpleTree.addDomHandler(new MouseOutHandler() {
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ VUIDLBrowser.deHiglight();
+ }
+ }, MouseOutEvent.getType());
+ connectors.add(connector);
+
+ String msg = indent + "* " + getConnectorString(connector);
+ GWT.log(msg);
+ consoleLog(msg);
+ System.out.println(msg);
+
+ for (ServerConnector c : connector.getChildren()) {
+ simpleTree.add(dumpConnectorHierarchy(c, indent + " ", connectors));
+ }
+ return simpleTree;
+ }
+
+ private static String getConnectorString(ServerConnector connector) {
+ return Util.getConnectorString(connector);
+ }
+
+ @Override
+ public void setQuietMode(boolean quietDebugMode) {
+ quietMode = quietDebugMode;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java b/client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java
new file mode 100644
index 0000000000..add6ee4780
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VErrorMessage.java
@@ -0,0 +1,61 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+public class VErrorMessage extends FlowPanel {
+ public static final String CLASSNAME = "v-errormessage";
+
+ public VErrorMessage() {
+ super();
+ setStyleName(CLASSNAME);
+ }
+
+ public void updateMessage(String htmlErrorMessage) {
+ clear();
+ if (htmlErrorMessage == null || htmlErrorMessage.length() == 0) {
+ add(new HTML(" "));
+ } else {
+ // pre-formatted on the server as div per child
+ add(new HTML(htmlErrorMessage));
+ }
+ }
+
+ /**
+ * Shows this error message next to given element.
+ *
+ * @param indicatorElement
+ */
+ public void showAt(Element indicatorElement) {
+ VOverlay errorContainer = (VOverlay) getParent();
+ if (errorContainer == null) {
+ errorContainer = new VOverlay();
+ 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 VOverlay errorContainer = (VOverlay) getParent();
+ if (errorContainer != null) {
+ errorContainer.hide();
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java b/client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java
new file mode 100644
index 0000000000..6f5e5854b2
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VSchedulerImpl.java
@@ -0,0 +1,33 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.impl.SchedulerImpl;
+
+public class VSchedulerImpl extends SchedulerImpl {
+
+ /**
+ * Keeps track of if there are deferred commands that are being executed. 0
+ * == no deferred commands currently in progress, > 0 otherwise.
+ */
+ private int deferredCommandTrackers = 0;
+
+ @Override
+ public void scheduleDeferred(ScheduledCommand cmd) {
+ deferredCommandTrackers++;
+ super.scheduleDeferred(cmd);
+ super.scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ deferredCommandTrackers--;
+ }
+ });
+ }
+
+ public boolean hasWorkQueued() {
+ boolean hasWorkQueued = (deferredCommandTrackers != 0);
+ return hasWorkQueued;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VTooltip.java b/client/src/com/vaadin/terminal/gwt/client/VTooltip.java
new file mode 100644
index 0000000000..a3523c2013
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VTooltip.java
@@ -0,0 +1,352 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+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.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+/**
+ * TODO open for extension
+ */
+public class VTooltip extends VOverlay {
+ private static final String CLASSNAME = "v-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;
+ VErrorMessage em = new VErrorMessage();
+ Element description = DOM.createDiv();
+
+ 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 VTooltip(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);
+ setSinkShadowEvents(true);
+ }
+
+ /**
+ * Show a popup containing the information in the "info" tooltip
+ *
+ * @param info
+ */
+ private void show(TooltipInfo info) {
+ boolean hasContent = false;
+ if (info.getErrorMessage() != null) {
+ em.setVisible(true);
+ em.updateMessage(info.getErrorMessage());
+ 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) {
+ // Issue #8454: With IE7 the tooltips size is calculated based on
+ // the last tooltip's position, causing problems if the last one was
+ // in the right or bottom edge. For this reason the tooltip is moved
+ // first to 0,0 position so that the calculation goes correctly.
+ setPopupPosition(0, 0);
+ setPopupPositionAndShow(new PositionCallback() {
+ @Override
+ public void setPosition(int offsetWidth, int offsetHeight) {
+
+ if (offsetWidth > MAX_WIDTH) {
+ setWidth(MAX_WIDTH + "px");
+
+ // Check new height and width with reflowed content
+ offsetWidth = getOffsetWidth();
+ offsetHeight = getOffsetHeight();
+ }
+
+ 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;
+ if (y - Window.getScrollTop() < 0) {
+ // tooltip does not fit on top of the mouse either,
+ // put it at the top of the screen
+ y = Window.getScrollTop();
+ }
+ }
+
+ setPopupPosition(x, y);
+ sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT);
+ }
+ });
+ } else {
+ hide();
+ }
+ }
+
+ private void showTooltip() {
+
+ // Close current tooltip
+ if (isShowing()) {
+ closeNow();
+ }
+
+ // Schedule timer for showing the tooltip according to if it was
+ // recently closed or not.
+ int timeout = justClosed ? QUICK_OPEN_DELAY : OPEN_DELAY;
+ showTimer.schedule(timeout);
+ opening = true;
+ }
+
+ private void closeNow() {
+ hide();
+ setWidth("");
+ closing = false;
+ }
+
+ private Timer showTimer = new Timer() {
+ @Override
+ public void run() {
+ TooltipInfo info = tooltipEventHandler.getTooltipInfo();
+ 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;
+ }
+ 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);
+ }
+
+ @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
+
+ if (type == Event.ONMOUSEOVER) {
+ // Cancel closing so tooltip stays open and user can copy paste the
+ // tooltip
+ closeTimer.cancel();
+ closing = false;
+ }
+ }
+
+ /**
+ * Replace current open tooltip with new content
+ */
+ public void replaceCurrentTooltip() {
+ if (closing) {
+ closeTimer.cancel();
+ closeNow();
+ }
+
+ TooltipInfo info = tooltipEventHandler.getTooltipInfo();
+ if (null != info) {
+ show(info);
+ }
+ opening = false;
+ }
+
+ private class TooltipEventHandler implements MouseMoveHandler,
+ ClickHandler, KeyDownHandler {
+
+ /**
+ * Current element hovered
+ */
+ private com.google.gwt.dom.client.Element currentElement = null;
+
+ /**
+ * Current tooltip active
+ */
+ private TooltipInfo currentTooltipInfo = null;
+
+ /**
+ * Get current active tooltip information
+ *
+ * @return Current active tooltip information or null
+ */
+ public TooltipInfo getTooltipInfo() {
+ return currentTooltipInfo;
+ }
+
+ /**
+ * Locate connector and it's tooltip for given element
+ *
+ * @param element
+ * Element used in search
+ * @return true if connector and tooltip found
+ */
+ private boolean resolveConnector(Element element) {
+
+ ComponentConnector connector = Util.getConnectorForElement(ac,
+ RootPanel.get(), element);
+
+ // Try to find first connector with proper tooltip info
+ TooltipInfo info = null;
+ while (connector != null) {
+
+ info = connector.getTooltipInfo(element);
+
+ if (info != null && info.hasMessage()) {
+ break;
+ }
+
+ if (!(connector.getParent() instanceof ComponentConnector)) {
+ connector = null;
+ info = null;
+ break;
+ }
+ connector = (ComponentConnector) connector.getParent();
+ }
+
+ if (connector != null && info != null) {
+ currentTooltipInfo = info;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle hide event
+ *
+ * @param event
+ * Event causing hide
+ */
+ private void handleHideEvent() {
+ hideTooltip();
+ currentTooltipInfo = null;
+ }
+
+ @Override
+ public void onMouseMove(MouseMoveEvent mme) {
+ Event event = Event.as(mme.getNativeEvent());
+ com.google.gwt.dom.client.Element element = Element.as(event
+ .getEventTarget());
+
+ // We can ignore move event if it's handled by move or over already
+ if (currentElement == element) {
+ return;
+ }
+ currentElement = element;
+
+ boolean connectorAndTooltipFound = resolveConnector((com.google.gwt.user.client.Element) element);
+ if (!connectorAndTooltipFound) {
+ if (isShowing()) {
+ handleHideEvent();
+ } else {
+ currentTooltipInfo = null;
+ }
+ } else {
+ updatePosition(event);
+ if (isShowing()) {
+ replaceCurrentTooltip();
+ } else {
+ showTooltip();
+ }
+ }
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ handleHideEvent();
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ handleHideEvent();
+ }
+ }
+
+ private final TooltipEventHandler tooltipEventHandler = new TooltipEventHandler();
+
+ /**
+ * Connects DOM handlers to widget that are needed for tooltip presentation.
+ *
+ * @param widget
+ * Widget which DOM handlers are connected
+ */
+ public void connectHandlersToWidget(Widget widget) {
+ widget.addDomHandler(tooltipEventHandler, MouseMoveEvent.getType());
+ widget.addDomHandler(tooltipEventHandler, ClickEvent.getType());
+ widget.addDomHandler(tooltipEventHandler, KeyDownEvent.getType());
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java b/client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java
new file mode 100644
index 0000000000..f7d43a1a12
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/VUIDLBrowser.java
@@ -0,0 +1,350 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+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.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+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.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.Connector;
+import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.window.VWindow;
+
+/**
+ * TODO Rename to something more Vaadin7-ish?
+ */
+public class VUIDLBrowser extends SimpleTree {
+ private static final String HELP = "Shift click handle to open recursively. "
+ + " Click components to highlight them on client side."
+ + " Shift click components to highlight them also on the server side.";
+ private ApplicationConnection client;
+ private String highlightedPid;
+
+ public VUIDLBrowser(final UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+ final UIDLItem root = new UIDLItem(uidl);
+ add(root);
+ }
+
+ public VUIDLBrowser(ValueMap u, ApplicationConnection client) {
+ this.client = client;
+ ValueMap valueMap = u.getValueMap("meta");
+ if (valueMap.containsKey("hl")) {
+ highlightedPid = valueMap.getString("hl");
+ }
+ Set<String> keySet = u.getKeySet();
+ for (String key : keySet) {
+ if (key.equals("state")) {
+ ValueMap stateJson = u.getValueMap(key);
+ SimpleTree stateChanges = new SimpleTree("shared state");
+
+ for (String connectorId : stateJson.getKeySet()) {
+ stateChanges.add(new SharedStateItem(connectorId, stateJson
+ .getValueMap(connectorId)));
+ }
+ add(stateChanges);
+
+ } else if (key.equals("changes")) {
+ JsArray<UIDL> jsValueMapArray = u.getJSValueMapArray(key)
+ .cast();
+ for (int i = 0; i < jsValueMapArray.length(); i++) {
+ UIDL uidl = jsValueMapArray.get(i);
+ UIDLItem change = new UIDLItem(uidl);
+ change.setTitle("change " + i);
+ add(change);
+ }
+ } else if (key.equals("meta")) {
+
+ } else {
+ // TODO consider pretty printing other request data such as
+ // hierarchy changes
+ // addItem(key + " : " + u.getAsString(key));
+ }
+ }
+ open(highlightedPid != null);
+ setTitle(HELP);
+ }
+
+ /**
+ * A debug view of a server-originated component state change.
+ */
+ abstract class StateChangeItem extends SimpleTree {
+
+ protected StateChangeItem() {
+ setTitle(HELP);
+
+ addDomHandler(new MouseOutHandler() {
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ deHiglight();
+ }
+ }, MouseOutEvent.getType());
+ }
+
+ @Override
+ protected void select(ClickEvent event) {
+ ComponentConnector connector = getConnector();
+ highlight(connector);
+ if (event != null && event.getNativeEvent().getShiftKey()) {
+ connector.getConnection().highlightComponent(connector);
+ }
+ super.select(event);
+ }
+
+ /**
+ * Returns the Connector associated with this state change.
+ */
+ protected ComponentConnector getConnector() {
+ Connector connector = client.getConnectorMap().getConnector(
+ getConnectorId());
+
+ if (connector instanceof ComponentConnector) {
+ return (ComponentConnector) connector;
+ } else {
+ return null;
+ }
+ }
+
+ protected abstract String getConnectorId();
+ }
+
+ /**
+ * A debug view of a Vaadin 7 style shared state change.
+ */
+ class SharedStateItem extends StateChangeItem {
+
+ private String connectorId;
+
+ SharedStateItem(String connectorId, ValueMap stateChanges) {
+ this.connectorId = connectorId;
+ ComponentConnector connector = getConnector();
+ if (connector != null) {
+ setText(Util.getConnectorString(connector));
+ } else {
+ setText("Unknown connector " + connectorId);
+ }
+ dir(new JSONObject(stateChanges), this);
+ }
+
+ @Override
+ protected String getConnectorId() {
+ return connectorId;
+ }
+
+ private void dir(String key, JSONValue value, SimpleTree tree) {
+ if (value.isObject() != null) {
+ SimpleTree subtree = new SimpleTree(key + "=object");
+ tree.add(subtree);
+ dir(value.isObject(), subtree);
+ } else if (value.isArray() != null) {
+ SimpleTree subtree = new SimpleTree(key + "=array");
+ dir(value.isArray(), subtree);
+ tree.add(subtree);
+ } else {
+ tree.addItem(key + "=" + value);
+ }
+ }
+
+ private void dir(JSONObject state, SimpleTree tree) {
+ for (String key : state.keySet()) {
+ dir(key, state.get(key), tree);
+ }
+ }
+
+ private void dir(JSONArray array, SimpleTree tree) {
+ for (int i = 0; i < array.size(); ++i) {
+ dir("" + i, array.get(i), tree);
+ }
+ }
+ }
+
+ /**
+ * A debug view of a Vaadin 6 style hierarchical component state change.
+ */
+ class UIDLItem extends StateChangeItem {
+
+ private UIDL uidl;
+
+ UIDLItem(UIDL uidl) {
+ this.uidl = uidl;
+ try {
+ String name = uidl.getTag();
+ try {
+ name = getNodeName(uidl, client.getConfiguration(),
+ Integer.parseInt(name));
+ } catch (Exception e) {
+ // NOP
+ }
+ setText(name);
+ addItem("LOADING");
+ } catch (Exception e) {
+ setText(uidl.toString());
+ }
+ }
+
+ @Override
+ protected String getConnectorId() {
+ return uidl.getId();
+ }
+
+ private String getNodeName(UIDL uidl, ApplicationConfiguration conf,
+ int tag) {
+ Class<? extends ServerConnector> widgetClassByDecodedTag = conf
+ .getConnectorClassByEncodedTag(tag);
+ if (widgetClassByDecodedTag == UnknownComponentConnector.class) {
+ return conf.getUnknownServerClassNameByTag(tag)
+ + "(NO CLIENT IMPLEMENTATION FOUND)";
+ } else {
+ return widgetClassByDecodedTag.getName();
+ }
+ }
+
+ @Override
+ public void open(boolean recursive) {
+ if (getWidgetCount() == 1
+ && getWidget(0).getElement().getInnerText()
+ .equals("LOADING")) {
+ dir();
+ }
+ super.open(recursive);
+ }
+
+ public void dir() {
+ remove(0);
+
+ String nodeName = uidl.getTag();
+ try {
+ nodeName = getNodeName(uidl, client.getConfiguration(),
+ Integer.parseInt(nodeName));
+ } catch (Exception e) {
+ // NOP
+ }
+
+ Set<String> attributeNames = uidl.getAttributeNames();
+ for (String name : attributeNames) {
+ if (uidl.isMapAttribute(name)) {
+ try {
+ ValueMap map = uidl.getMapAttribute(name);
+ JsArrayString keyArray = map.getKeyArray();
+ nodeName += " " + name + "=" + "{";
+ for (int i = 0; i < keyArray.length(); i++) {
+ nodeName += keyArray.get(i) + ":"
+ + map.getAsString(keyArray.get(i)) + ",";
+ }
+ nodeName += "}";
+ } catch (Exception e) {
+
+ }
+ } else {
+ final String value = uidl.getAttribute(name);
+ nodeName += " " + name + "=" + value;
+ }
+ }
+ setText(nodeName);
+
+ try {
+ SimpleTree tmp = null;
+ Set<String> variableNames = uidl.getVariableNames();
+ for (String name : variableNames) {
+ String value = "";
+ try {
+ value = uidl.getVariable(name);
+ } catch (final Exception e) {
+ try {
+ String[] stringArrayAttribute = uidl
+ .getStringArrayAttribute(name);
+ value = stringArrayAttribute.toString();
+ } catch (final Exception e2) {
+ try {
+ final int intVal = uidl.getIntVariable(name);
+ value = String.valueOf(intVal);
+ } catch (final Exception e3) {
+ value = "unknown";
+ }
+ }
+ }
+ if (tmp == null) {
+ tmp = new SimpleTree("variables");
+ }
+ tmp.addItem(name + "=" + value);
+ }
+ if (tmp != null) {
+ add(tmp);
+ }
+ } catch (final Exception e) {
+ // Ignored, no variables
+ }
+
+ final Iterator<Object> i = uidl.getChildIterator();
+ while (i.hasNext()) {
+ final Object child = i.next();
+ try {
+ add(new UIDLItem((UIDL) child));
+ } catch (final Exception e) {
+ addItem(child.toString());
+ }
+ }
+ if (highlightedPid != null && highlightedPid.equals(uidl.getId())) {
+ getElement().getStyle().setBackgroundColor("#fdd");
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ getElement().scrollIntoView();
+ }
+ });
+ }
+ }
+ }
+
+ static Element highlight = Document.get().createDivElement();
+
+ static {
+ Style style = highlight.getStyle();
+ style.setPosition(Position.ABSOLUTE);
+ style.setZIndex(VWindow.Z_INDEX + 1000);
+ style.setBackgroundColor("red");
+ style.setOpacity(0.2);
+ if (BrowserInfo.get().isIE()) {
+ style.setProperty("filter", "alpha(opacity=20)");
+ }
+ }
+
+ static void highlight(ComponentConnector paintable) {
+ if (paintable != null) {
+ Widget w = paintable.getWidget();
+ Style style = highlight.getStyle();
+ style.setTop(w.getAbsoluteTop(), Unit.PX);
+ style.setLeft(w.getAbsoluteLeft(), Unit.PX);
+ style.setWidth(w.getOffsetWidth(), Unit.PX);
+ style.setHeight(w.getOffsetHeight(), Unit.PX);
+ RootPanel.getBodyElement().appendChild(highlight);
+ }
+ }
+
+ static void deHiglight() {
+ if (highlight.getParentElement() != null) {
+ highlight.getParentElement().removeChild(highlight);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ValueMap.java b/client/src/com/vaadin/terminal/gwt/client/ValueMap.java
new file mode 100644
index 0000000000..5deb5feb55
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ValueMap.java
@@ -0,0 +1,109 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+
+public final class ValueMap extends JavaScriptObject {
+ protected ValueMap() {
+ }
+
+ public native double getRawNumber(final String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+ public native int getInt(final String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+ public native boolean getBoolean(final String name)
+ /*-{
+ return Boolean(this[name]);
+ }-*/;
+
+ public native String getString(String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+ public native JsArrayString getKeyArray()
+ /*-{
+ var a = new Array();
+ var attr = this;
+ for(var j in attr) {
+ // workaround for the infamous chrome hosted mode hack (__gwt_ObjectId)
+ if(attr.hasOwnProperty(j))
+ a.push(j);
+ }
+ return a;
+ }-*/;
+
+ public Set<String> getKeySet() {
+ final HashSet<String> attrs = new HashSet<String>();
+ JsArrayString attributeNamesArray = getKeyArray();
+ for (int i = 0; i < attributeNamesArray.length(); i++) {
+ attrs.add(attributeNamesArray.get(i));
+ }
+ return attrs;
+ }
+
+ native JsArrayString getJSStringArray(String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+ native JsArray<ValueMap> getJSValueMapArray(String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+ public String[] getStringArray(final String name) {
+ JsArrayString stringArrayAttribute = getJSStringArray(name);
+ final String[] s = new String[stringArrayAttribute.length()];
+ for (int i = 0; i < stringArrayAttribute.length(); i++) {
+ s[i] = stringArrayAttribute.get(i);
+ }
+ return s;
+ }
+
+ public int[] getIntArray(final String name) {
+ JsArrayString stringArrayAttribute = getJSStringArray(name);
+ final int[] s = new int[stringArrayAttribute.length()];
+ for (int i = 0; i < stringArrayAttribute.length(); i++) {
+ s[i] = Integer.parseInt(stringArrayAttribute.get(i));
+ }
+ return s;
+ }
+
+ public native boolean containsKey(final String name)
+ /*-{
+ return name in this;
+ }-*/;
+
+ public native ValueMap getValueMap(String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+ native String getAsString(String name)
+ /*-{
+ return '' + this[name];
+ }-*/;
+
+ native JavaScriptObject getJavaScriptObject(String name)
+ /*-{
+ return this[name];
+ }-*/;
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java b/client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java
new file mode 100644
index 0000000000..0a4f92bc79
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/WidgetInstantiator.java
@@ -0,0 +1,11 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+/**
+ * A helper class used by WidgetMap implementation. Used by the generated code.
+ */
+interface WidgetInstantiator {
+ public ServerConnector get();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java b/client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java
new file mode 100644
index 0000000000..749a8343c8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/WidgetLoader.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.RunAsyncCallback;
+
+/** A helper class used by WidgetMap implementation. Used by the generated code. */
+abstract class WidgetLoader implements RunAsyncCallback {
+
+ @Override
+ public void onFailure(Throwable reason) {
+ ApplicationConfiguration.endDependencyLoading();
+ }
+
+ @Override
+ public void onSuccess() {
+ addInstantiator();
+ ApplicationConfiguration.endDependencyLoading();
+ }
+
+ abstract void addInstantiator();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetMap.java b/client/src/com/vaadin/terminal/gwt/client/WidgetMap.java
new file mode 100644
index 0000000000..b770414457
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/WidgetMap.java
@@ -0,0 +1,65 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+
+import com.vaadin.terminal.gwt.widgetsetutils.WidgetMapGenerator;
+
+/**
+ * Abstract class mapping between {@link ComponentConnector} instances and their
+ * instances.
+ *
+ * A concrete implementation of this class is generated by
+ * {@link WidgetMapGenerator} or one of its subclasses during widgetset
+ * compilation.
+ */
+abstract class WidgetMap {
+
+ protected static HashMap<Class<? extends ServerConnector>, WidgetInstantiator> instmap = new HashMap<Class<? extends ServerConnector>, WidgetInstantiator>();
+
+ /**
+ * Create a new instance of a connector based on its type.
+ *
+ * @param classType
+ * {@link ComponentConnector} class to instantiate
+ * @return new instance of the connector
+ */
+ public ServerConnector instantiate(
+ Class<? extends ServerConnector> classType) {
+ return instmap.get(classType).get();
+ }
+
+ /**
+ * Return the connector class to use for a fully qualified server side
+ * component class name.
+ *
+ * @param fullyqualifiedName
+ * fully qualified name of the server side component class
+ * @return component connector class to use
+ */
+ public abstract Class<? extends ServerConnector> getConnectorClassForServerSideClassName(
+ String fullyqualifiedName);
+
+ /**
+ * Return the connector classes to load after the initial widgetset load and
+ * start.
+ *
+ * @return component connector class to load after the initial widgetset
+ * loading
+ */
+ public abstract Class<? extends ServerConnector>[] getDeferredLoadedConnectors();
+
+ /**
+ * Make sure the code for a (deferred or lazy) component connector type has
+ * been loaded, triggering the load and waiting for its completion if
+ * necessary.
+ *
+ * @param classType
+ * component connector class
+ */
+ public abstract void ensureInstantiator(
+ Class<? extends ServerConnector> classType);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/WidgetSet.java b/client/src/com/vaadin/terminal/gwt/client/WidgetSet.java
new file mode 100644
index 0000000000..3d7e838c62
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/WidgetSet.java
@@ -0,0 +1,115 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector;
+
+public class WidgetSet {
+
+ /**
+ * WidgetSet (and its extensions) delegate instantiation of widgets and
+ * client-server matching to WidgetMap. The actual implementations are
+ * generated with gwts generators/deferred binding.
+ */
+ private WidgetMap widgetMap = GWT.create(WidgetMap.class);
+
+ /**
+ * Create an uninitialized connector that best matches given UIDL. The
+ * connector must implement {@link ServerConnector}.
+ *
+ * @param tag
+ * connector type tag for the connector to create
+ * @param conf
+ * the application configuration to use when creating the
+ * connector
+ *
+ * @return New uninitialized and unregistered connector that can paint given
+ * UIDL.
+ */
+ public ServerConnector createConnector(int tag,
+ ApplicationConfiguration conf) {
+ /*
+ * Yes, this (including the generated code in WidgetMap) may look very
+ * odd code, but due the nature of GWT, we cannot do this any cleaner.
+ * Luckily this is mostly written by WidgetSetGenerator, here are just
+ * some hacks. Extra instantiation code is needed if client side
+ * connector has no "native" counterpart on client side.
+ */
+
+ Class<? extends ServerConnector> classType = resolveInheritedConnectorType(
+ conf, tag);
+
+ if (classType == null || classType == UnknownComponentConnector.class) {
+ String serverSideName = conf.getUnknownServerClassNameByTag(tag);
+ UnknownComponentConnector c = GWT
+ .create(UnknownComponentConnector.class);
+ c.setServerSideClassName(serverSideName);
+ return c;
+ } else {
+ /*
+ * let the auto generated code instantiate this type
+ */
+ ServerConnector connector = widgetMap.instantiate(classType);
+ if (connector instanceof HasJavaScriptConnectorHelper) {
+ ((HasJavaScriptConnectorHelper) connector)
+ .getJavascriptConnectorHelper().setTag(tag);
+ }
+ return connector;
+ }
+ }
+
+ private Class<? extends ServerConnector> resolveInheritedConnectorType(
+ ApplicationConfiguration conf, int tag) {
+ Class<? extends ServerConnector> classType = null;
+ Integer t = tag;
+ do {
+ classType = resolveConnectorType(t, conf);
+ t = conf.getParentTag(t);
+ } while (classType == null && t != null);
+ return classType;
+ }
+
+ protected Class<? extends ServerConnector> resolveConnectorType(int tag,
+ ApplicationConfiguration conf) {
+ Class<? extends ServerConnector> connectorClass = conf
+ .getConnectorClassByEncodedTag(tag);
+
+ return connectorClass;
+ }
+
+ /**
+ * Due its nature, GWT does not support dynamic classloading. To bypass this
+ * limitation, widgetset must have function that returns Class by its fully
+ * qualified name.
+ *
+ * @param tag
+ * @param applicationConfiguration
+ * @return
+ */
+ public Class<? extends ServerConnector> getConnectorClassByTag(int tag,
+ ApplicationConfiguration conf) {
+ Class<? extends ServerConnector> connectorClass = null;
+ Integer t = tag;
+ do {
+ String serverSideClassName = conf.getServerSideClassNameForTag(t);
+ connectorClass = widgetMap
+ .getConnectorClassForServerSideClassName(serverSideClassName);
+ t = conf.getParentTag(t);
+ } while (connectorClass == UnknownComponentConnector.class && t != null);
+
+ return connectorClass;
+ }
+
+ public Class<? extends ServerConnector>[] getDeferredLoadedConnectors() {
+ return widgetMap.getDeferredLoadedConnectors();
+ }
+
+ public void loadImplementation(Class<? extends ServerConnector> nextType) {
+ widgetMap.ensureInstantiator(nextType);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java b/client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java
new file mode 100644
index 0000000000..b465e3ad7e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/AbstractServerConnectorEvent.java
@@ -0,0 +1,33 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+public abstract class AbstractServerConnectorEvent<H extends EventHandler>
+ extends GwtEvent<H> {
+ private ServerConnector connector;
+
+ protected AbstractServerConnectorEvent() {
+ }
+
+ public ServerConnector getConnector() {
+ return connector;
+ }
+
+ public void setConnector(ServerConnector connector) {
+ this.connector = connector;
+ }
+
+ /**
+ * Sends this event to the given handler.
+ *
+ * @param handler
+ * The handler to dispatch.
+ */
+ @Override
+ public abstract void dispatch(H handler);
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java b/client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java
new file mode 100644
index 0000000000..29cb714828
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/DiffJSONSerializer.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.google.gwt.json.client.JSONValue;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+public interface DiffJSONSerializer<T> extends JSONSerializer<T> {
+ /**
+ * Update the target object in place based on the passed JSON data.
+ *
+ * @param target
+ * @param jsonValue
+ * @param connection
+ */
+ public void update(T target, Type type, JSONValue jsonValue,
+ ApplicationConnection connection);
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java b/client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java
new file mode 100644
index 0000000000..c92466084c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/GeneratedRpcMethodProvider.java
@@ -0,0 +1,20 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import java.util.Collection;
+
+/**
+ * Provides runtime data about client side RPC calls received from the server to
+ * the client-side code.
+ *
+ * A GWT generator is used to create an implementation of this class at
+ * run-time.
+ *
+ * @since 7.0
+ */
+public interface GeneratedRpcMethodProvider {
+
+ public Collection<RpcMethod> getGeneratedRpcMethods();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java b/client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java
new file mode 100644
index 0000000000..a5191a5fed
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/HasJavaScriptConnectorHelper.java
@@ -0,0 +1,11 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper;
+
+public interface HasJavaScriptConnectorHelper {
+ public JavaScriptConnectorHelper getJavascriptConnectorHelper();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java b/client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java
new file mode 100644
index 0000000000..f1b6b44b7d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/InitializableServerRpc.java
@@ -0,0 +1,27 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * Initialization support for client to server RPC interfaces.
+ *
+ * This is in a separate interface used by the GWT generator class. The init
+ * method is not in {@link ServerRpc} because then also server side proxies
+ * would have to implement the initialization method.
+ *
+ * @since 7.0
+ */
+public interface InitializableServerRpc extends ServerRpc {
+ /**
+ * Associates the RPC proxy with a connector. Called by generated code.
+ * Should never be called manually.
+ *
+ * @param connector
+ * The connector the ServerRPC instance is assigned to.
+ */
+ public void initRpc(ServerConnector connector);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java b/client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java
new file mode 100644
index 0000000000..9820b6a895
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/JSONSerializer.java
@@ -0,0 +1,54 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.server.JsonCodec;
+
+/**
+ * Implementors of this interface knows how to serialize an Object of a given
+ * type to JSON and how to deserialize the JSON back into an object.
+ *
+ * The {@link #serialize(Object, ConnectorMap)} and
+ * {@link #deserialize(JSONObject, ConnectorMap)} methods must be symmetric so
+ * they can be chained and produce the original result (or an equal result).
+ *
+ * Each {@link JSONSerializer} implementation can handle an object of a single
+ * type - see {@link SerializerMap}.
+ *
+ * @since 7.0
+ */
+public interface JSONSerializer<T> {
+
+ /**
+ * Creates and deserializes an object received from the server. Must be
+ * compatible with {@link #serialize(Object, ConnectorMap)} and also with
+ * the server side
+ * {@link JsonCodec#encode(Object, com.vaadin.terminal.gwt.server.PaintableIdMapper)}
+ * .
+ *
+ * @param jsonValue
+ * JSON map from property name to property value
+ * @return A deserialized object
+ */
+ T deserialize(Type type, JSONValue jsonValue,
+ ApplicationConnection connection);
+
+ /**
+ * Serialize the given object into JSON. Must be compatible with
+ * {@link #deserialize(JSONObject, ConnectorMap)} and also with the server
+ * side
+ * {@link JsonCodec#decode(com.vaadin.external.json.JSONArray, com.vaadin.terminal.gwt.server.PaintableIdMapper)}
+ *
+ * @param value
+ * The object to serialize
+ * @return A JSON serialized version of the object
+ */
+ JSONValue serialize(T value, ApplicationConnection connection);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java b/client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java
new file mode 100644
index 0000000000..7268acdfc5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java
@@ -0,0 +1,213 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.communication;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.vaadin.shared.Connector;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+
+/**
+ * Client side decoder for decodeing shared state and other values from JSON
+ * received from the server.
+ *
+ * Currently, basic data types as well as Map, String[] and Object[] are
+ * supported, where maps and Object[] can contain other supported data types.
+ *
+ * TODO extensible type support
+ *
+ * @since 7.0
+ */
+public class JsonDecoder {
+
+ /**
+ * Decode a JSON array with two elements (type and value) into a client-side
+ * type, recursively if necessary.
+ *
+ * @param jsonValue
+ * JSON value with encoded data
+ * @param connection
+ * reference to the current ApplicationConnection
+ * @return decoded value (does not contain JSON types)
+ */
+ public static Object decodeValue(Type type, JSONValue jsonValue,
+ Object target, ApplicationConnection connection) {
+
+ // Null is null, regardless of type
+ if (jsonValue.isNull() != null) {
+ return null;
+ }
+
+ String baseTypeName = type.getBaseTypeName();
+ if (Map.class.getName().equals(baseTypeName)
+ || HashMap.class.getName().equals(baseTypeName)) {
+ return decodeMap(type, jsonValue, connection);
+ } else if (List.class.getName().equals(baseTypeName)
+ || ArrayList.class.getName().equals(baseTypeName)) {
+ return decodeList(type, (JSONArray) jsonValue, connection);
+ } else if (Set.class.getName().equals(baseTypeName)) {
+ return decodeSet(type, (JSONArray) jsonValue, connection);
+ } else if (String.class.getName().equals(baseTypeName)) {
+ return ((JSONString) jsonValue).stringValue();
+ } else if (Integer.class.getName().equals(baseTypeName)) {
+ return Integer.valueOf(String.valueOf(jsonValue));
+ } else if (Long.class.getName().equals(baseTypeName)) {
+ // TODO handle properly
+ return Long.valueOf(String.valueOf(jsonValue));
+ } else if (Float.class.getName().equals(baseTypeName)) {
+ // TODO handle properly
+ return Float.valueOf(String.valueOf(jsonValue));
+ } else if (Double.class.getName().equals(baseTypeName)) {
+ // TODO handle properly
+ return Double.valueOf(String.valueOf(jsonValue));
+ } else if (Boolean.class.getName().equals(baseTypeName)) {
+ // TODO handle properly
+ return Boolean.valueOf(String.valueOf(jsonValue));
+ } else if (Byte.class.getName().equals(baseTypeName)) {
+ // TODO handle properly
+ return Byte.valueOf(String.valueOf(jsonValue));
+ } else if (Character.class.getName().equals(baseTypeName)) {
+ // TODO handle properly
+ return Character.valueOf(((JSONString) jsonValue).stringValue()
+ .charAt(0));
+ } else if (Connector.class.getName().equals(baseTypeName)) {
+ return ConnectorMap.get(connection).getConnector(
+ ((JSONString) jsonValue).stringValue());
+ } else {
+ return decodeObject(type, jsonValue, target, connection);
+ }
+ }
+
+ private static Object decodeObject(Type type, JSONValue jsonValue,
+ Object target, ApplicationConnection connection) {
+ JSONSerializer<Object> serializer = connection.getSerializerMap()
+ .getSerializer(type.getBaseTypeName());
+ // TODO handle case with no serializer found
+ // Currently getSerializer throws exception if not found
+
+ if (target != null && serializer instanceof DiffJSONSerializer<?>) {
+ DiffJSONSerializer<Object> diffSerializer = (DiffJSONSerializer<Object>) serializer;
+ diffSerializer.update(target, type, jsonValue, connection);
+ return target;
+ } else {
+ Object object = serializer.deserialize(type, jsonValue, connection);
+ return object;
+ }
+ }
+
+ private static Map<Object, Object> decodeMap(Type type, JSONValue jsonMap,
+ ApplicationConnection connection) {
+ // Client -> server encodes empty map as an empty array because of
+ // #8906. Do the same for server -> client to maintain symmetry.
+ if (jsonMap instanceof JSONArray) {
+ JSONArray array = (JSONArray) jsonMap;
+ if (array.size() == 0) {
+ return new HashMap<Object, Object>();
+ }
+ }
+
+ Type keyType = type.getParameterTypes()[0];
+ Type valueType = type.getParameterTypes()[1];
+
+ if (keyType.getBaseTypeName().equals(String.class.getName())) {
+ return decodeStringMap(valueType, jsonMap, connection);
+ } else if (keyType.getBaseTypeName().equals(Connector.class.getName())) {
+ return decodeConnectorMap(valueType, jsonMap, connection);
+ } else {
+ return decodeObjectMap(keyType, valueType, jsonMap, connection);
+ }
+ }
+
+ private static Map<Object, Object> decodeObjectMap(Type keyType,
+ Type valueType, JSONValue jsonValue,
+ ApplicationConnection connection) {
+ Map<Object, Object> map = new HashMap<Object, Object>();
+
+ JSONArray mapArray = (JSONArray) jsonValue;
+ JSONArray keys = (JSONArray) mapArray.get(0);
+ JSONArray values = (JSONArray) mapArray.get(1);
+
+ assert (keys.size() == values.size());
+
+ for (int i = 0; i < keys.size(); i++) {
+ Object decodedKey = decodeValue(keyType, keys.get(i), null,
+ connection);
+ Object decodedValue = decodeValue(valueType, values.get(i), null,
+ connection);
+
+ map.put(decodedKey, decodedValue);
+ }
+
+ return map;
+ }
+
+ private static Map<Object, Object> decodeConnectorMap(Type valueType,
+ JSONValue jsonValue, ApplicationConnection connection) {
+ Map<Object, Object> map = new HashMap<Object, Object>();
+
+ JSONObject jsonMap = (JSONObject) jsonValue;
+ ConnectorMap connectorMap = ConnectorMap.get(connection);
+
+ for (String connectorId : jsonMap.keySet()) {
+ Object value = decodeValue(valueType, jsonMap.get(connectorId),
+ null, connection);
+ map.put(connectorMap.getConnector(connectorId), value);
+ }
+
+ return map;
+ }
+
+ private static Map<Object, Object> decodeStringMap(Type valueType,
+ JSONValue jsonValue, ApplicationConnection connection) {
+ Map<Object, Object> map = new HashMap<Object, Object>();
+
+ JSONObject jsonMap = (JSONObject) jsonValue;
+
+ for (String key : jsonMap.keySet()) {
+ Object value = decodeValue(valueType, jsonMap.get(key), null,
+ connection);
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ private static List<Object> decodeList(Type type, JSONArray jsonArray,
+ ApplicationConnection connection) {
+ List<Object> tokens = new ArrayList<Object>();
+ decodeIntoCollection(type.getParameterTypes()[0], jsonArray,
+ connection, tokens);
+ return tokens;
+ }
+
+ private static Set<Object> decodeSet(Type type, JSONArray jsonArray,
+ ApplicationConnection connection) {
+ Set<Object> tokens = new HashSet<Object>();
+ decodeIntoCollection(type.getParameterTypes()[0], jsonArray,
+ connection, tokens);
+ return tokens;
+ }
+
+ private static void decodeIntoCollection(Type childType,
+ JSONArray jsonArray, ApplicationConnection connection,
+ Collection<Object> tokens) {
+ for (int i = 0; i < jsonArray.size(); ++i) {
+ // each entry always has two elements: type and value
+ JSONValue entryValue = jsonArray.get(i);
+ tokens.add(decodeValue(childType, entryValue, null, connection));
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java b/client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java
new file mode 100644
index 0000000000..404f1238e0
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java
@@ -0,0 +1,286 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.communication;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONNull;
+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.vaadin.shared.Connector;
+import com.vaadin.shared.communication.UidlValue;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+/**
+ * Encoder for converting RPC parameters and other values to JSON for transfer
+ * between the client and the server.
+ *
+ * Currently, basic data types as well as Map, String[] and Object[] are
+ * supported, where maps and Object[] can contain other supported data types.
+ *
+ * TODO extensible type support
+ *
+ * @since 7.0
+ */
+public class JsonEncoder {
+
+ public static final String VTYPE_CONNECTOR = "c";
+ public static final String VTYPE_BOOLEAN = "b";
+ public static final String VTYPE_DOUBLE = "d";
+ public static final String VTYPE_FLOAT = "f";
+ public static final String VTYPE_LONG = "l";
+ public static final String VTYPE_INTEGER = "i";
+ public static final String VTYPE_STRING = "s";
+ public static final String VTYPE_ARRAY = "a";
+ public static final String VTYPE_STRINGARRAY = "S";
+ public static final String VTYPE_MAP = "m";
+ public static final String VTYPE_LIST = "L";
+ public static final String VTYPE_SET = "q";
+ public static final String VTYPE_NULL = "n";
+
+ /**
+ * Encode a value to a JSON representation for transport from the client to
+ * the server.
+ *
+ * @param value
+ * value to convert
+ * @param connection
+ * @return JSON representation of the value
+ */
+ public static JSONValue encode(Object value,
+ boolean restrictToInternalTypes, ApplicationConnection connection) {
+ if (null == value) {
+ return JSONNull.getInstance();
+ } else if (value instanceof JSONValue) {
+ return (JSONValue) value;
+ } else if (value instanceof String[]) {
+ String[] array = (String[]) value;
+ JSONArray jsonArray = new JSONArray();
+ for (int i = 0; i < array.length; ++i) {
+ jsonArray.set(i, new JSONString(array[i]));
+ }
+ return jsonArray;
+ } else if (value instanceof String) {
+ return new JSONString((String) value);
+ } else if (value instanceof Boolean) {
+ return JSONBoolean.getInstance((Boolean) value);
+ } else if (value instanceof Byte) {
+ return new JSONNumber((Byte) value);
+ } else if (value instanceof Character) {
+ return new JSONString(String.valueOf(value));
+ } else if (value instanceof Object[]) {
+ return encodeObjectArray((Object[]) value, restrictToInternalTypes,
+ connection);
+ } else if (value instanceof Enum) {
+ return encodeEnum((Enum<?>) value, connection);
+ } else if (value instanceof Map) {
+ return encodeMap((Map) value, restrictToInternalTypes, connection);
+ } else if (value instanceof Connector) {
+ Connector connector = (Connector) value;
+ return new JSONString(connector.getConnectorId());
+ } else if (value instanceof Collection) {
+ return encodeCollection((Collection) value,
+ restrictToInternalTypes, connection);
+ } else if (value instanceof UidlValue) {
+ return encodeVariableChange((UidlValue) value, connection);
+ } else {
+ String transportType = getTransportType(value);
+ if (transportType != null) {
+ return new JSONString(String.valueOf(value));
+ } else {
+ // Try to find a generated serializer object, class name is the
+ // type
+ transportType = value.getClass().getName();
+ JSONSerializer serializer = connection.getSerializerMap()
+ .getSerializer(transportType);
+
+ // TODO handle case with no serializer found
+ return serializer.serialize(value, connection);
+ }
+ }
+ }
+
+ private static JSONValue encodeVariableChange(UidlValue uidlValue,
+ ApplicationConnection connection) {
+ Object value = uidlValue.getValue();
+
+ JSONArray jsonArray = new JSONArray();
+ jsonArray.set(0, new JSONString(getTransportType(value)));
+ jsonArray.set(1, encode(value, true, connection));
+
+ return jsonArray;
+ }
+
+ private static JSONValue encodeMap(Map<Object, Object> map,
+ boolean restrictToInternalTypes, ApplicationConnection connection) {
+ /*
+ * As we have no info about declared types, we instead select encoding
+ * scheme based on actual type of first key. We can't do this if there's
+ * no first key, so instead we send some special value that the
+ * server-side decoding must check for. (see #8906)
+ */
+ if (map.isEmpty()) {
+ return new JSONArray();
+ }
+
+ Object firstKey = map.keySet().iterator().next();
+ if (firstKey instanceof String) {
+ return encodeStringMap(map, restrictToInternalTypes, connection);
+ } else if (restrictToInternalTypes) {
+ throw new IllegalStateException(
+ "Only string keys supported for legacy maps");
+ } else if (firstKey instanceof Connector) {
+ return encodeConenctorMap(map, connection);
+ } else {
+ return encodeObjectMap(map, connection);
+ }
+ }
+
+ private static JSONValue encodeObjectMap(Map<Object, Object> map,
+ ApplicationConnection connection) {
+ JSONArray keys = new JSONArray();
+ JSONArray values = new JSONArray();
+ for (Entry<?, ?> entry : map.entrySet()) {
+ // restrictToInternalTypes always false if we end up here
+ keys.set(keys.size(), encode(entry.getKey(), false, connection));
+ values.set(values.size(),
+ encode(entry.getValue(), false, connection));
+ }
+
+ JSONArray keysAndValues = new JSONArray();
+ keysAndValues.set(0, keys);
+ keysAndValues.set(1, values);
+
+ return keysAndValues;
+ }
+
+ private static JSONValue encodeConenctorMap(Map<Object, Object> map,
+ ApplicationConnection connection) {
+ JSONObject jsonMap = new JSONObject();
+
+ for (Entry<?, ?> entry : map.entrySet()) {
+ Connector connector = (Connector) entry.getKey();
+
+ // restrictToInternalTypes always false if we end up here
+ JSONValue encodedValue = encode(entry.getValue(), false, connection);
+
+ jsonMap.put(connector.getConnectorId(), encodedValue);
+ }
+
+ return jsonMap;
+ }
+
+ private static JSONValue encodeStringMap(Map<Object, Object> map,
+ boolean restrictToInternalTypes, ApplicationConnection connection) {
+ JSONObject jsonMap = new JSONObject();
+
+ for (Entry<?, ?> entry : map.entrySet()) {
+ String key = (String) entry.getKey();
+ Object value = entry.getValue();
+
+ if (restrictToInternalTypes) {
+ value = new UidlValue(value);
+ }
+
+ JSONValue encodedValue = encode(value, restrictToInternalTypes,
+ connection);
+
+ jsonMap.put(key, encodedValue);
+ }
+
+ return jsonMap;
+ }
+
+ private static JSONValue encodeEnum(Enum<?> e,
+ ApplicationConnection connection) {
+ return new JSONString(e.toString());
+ }
+
+ private static JSONValue encodeObjectArray(Object[] array,
+ boolean restrictToInternalTypes, ApplicationConnection connection) {
+ JSONArray jsonArray = new JSONArray();
+ for (int i = 0; i < array.length; ++i) {
+ // TODO handle object graph loops?
+ Object value = array[i];
+ if (restrictToInternalTypes) {
+ value = new UidlValue(value);
+ }
+ jsonArray
+ .set(i, encode(value, restrictToInternalTypes, connection));
+ }
+ return jsonArray;
+ }
+
+ private static JSONValue encodeCollection(Collection collection,
+ boolean restrictToInternalTypes, ApplicationConnection connection) {
+ JSONArray jsonArray = new JSONArray();
+ int idx = 0;
+ for (Object o : collection) {
+ JSONValue encodedObject = encode(o, restrictToInternalTypes,
+ connection);
+ jsonArray.set(idx++, encodedObject);
+ }
+ if (collection instanceof Set) {
+ return jsonArray;
+ } else if (collection instanceof List) {
+ return jsonArray;
+ } else {
+ throw new RuntimeException("Unsupport collection type: "
+ + collection.getClass().getName());
+ }
+
+ }
+
+ /**
+ * Returns the transport type for the given value. Only returns a transport
+ * type for internally handled values.
+ *
+ * @param value
+ * The value that should be transported
+ * @return One of the JsonEncode.VTYPE_ constants or null if the value
+ * cannot be transported using an internally handled type.
+ */
+ private static String getTransportType(Object value) {
+ if (value == null) {
+ return VTYPE_NULL;
+ } else if (value instanceof String) {
+ return VTYPE_STRING;
+ } else if (value instanceof Connector) {
+ return VTYPE_CONNECTOR;
+ } else if (value instanceof Boolean) {
+ return VTYPE_BOOLEAN;
+ } else if (value instanceof Integer) {
+ return VTYPE_INTEGER;
+ } else if (value instanceof Float) {
+ return VTYPE_FLOAT;
+ } else if (value instanceof Double) {
+ return VTYPE_DOUBLE;
+ } else if (value instanceof Long) {
+ return VTYPE_LONG;
+ } else if (value instanceof List) {
+ return VTYPE_LIST;
+ } else if (value instanceof Set) {
+ return VTYPE_SET;
+ } else if (value instanceof String[]) {
+ return VTYPE_STRINGARRAY;
+ } else if (value instanceof Object[]) {
+ return VTYPE_ARRAY;
+ } else if (value instanceof Map) {
+ return VTYPE_MAP;
+ } else if (value instanceof Enum<?>) {
+ // Enum value is processed as a string
+ return VTYPE_STRING;
+ }
+ return null;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java b/client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java
new file mode 100644
index 0000000000..b1c91fe049
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java
@@ -0,0 +1,124 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.communication;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONString;
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.VConsole;
+
+/**
+ * Client side RPC manager that can invoke methods based on RPC calls received
+ * from the server.
+ *
+ * A GWT generator is used to create an implementation of this class at
+ * run-time.
+ *
+ * @since 7.0
+ */
+public class RpcManager {
+
+ private final Map<String, RpcMethod> methodMap = new HashMap<String, RpcMethod>();
+
+ public RpcManager() {
+ GeneratedRpcMethodProvider provider = GWT
+ .create(GeneratedRpcMethodProvider.class);
+ Collection<RpcMethod> methods = provider.getGeneratedRpcMethods();
+ for (RpcMethod rpcMethod : methods) {
+ methodMap.put(
+ rpcMethod.getInterfaceName() + "."
+ + rpcMethod.getMethodName(), rpcMethod);
+ }
+ }
+
+ /**
+ * Perform server to client RPC invocation.
+ *
+ * @param invocation
+ * method to invoke
+ */
+ public void applyInvocation(MethodInvocation invocation,
+ ServerConnector connector) {
+ String signature = getSignature(invocation);
+
+ RpcMethod rpcMethod = getRpcMethod(signature);
+ Collection<ClientRpc> implementations = connector
+ .getRpcImplementations(invocation.getInterfaceName());
+ for (ClientRpc clientRpc : implementations) {
+ rpcMethod.applyInvocation(clientRpc, invocation.getParameters());
+ }
+ }
+
+ private RpcMethod getRpcMethod(String signature) {
+ RpcMethod rpcMethod = methodMap.get(signature);
+ if (rpcMethod == null) {
+ throw new IllegalStateException("There is no information about "
+ + signature
+ + ". Did you remember to compile the right widgetset?");
+ }
+ return rpcMethod;
+ }
+
+ private static String getSignature(MethodInvocation invocation) {
+ return invocation.getInterfaceName() + "." + invocation.getMethodName();
+ }
+
+ public Type[] getParameterTypes(MethodInvocation invocation) {
+ return getRpcMethod(getSignature(invocation)).getParameterTypes();
+ }
+
+ public void parseAndApplyInvocation(JSONArray rpcCall,
+ ApplicationConnection connection) {
+ ConnectorMap connectorMap = ConnectorMap.get(connection);
+
+ String connectorId = ((JSONString) rpcCall.get(0)).stringValue();
+ String interfaceName = ((JSONString) rpcCall.get(1)).stringValue();
+ String methodName = ((JSONString) rpcCall.get(2)).stringValue();
+ JSONArray parametersJson = (JSONArray) rpcCall.get(3);
+
+ ServerConnector connector = connectorMap.getConnector(connectorId);
+
+ MethodInvocation invocation = new MethodInvocation(connectorId,
+ interfaceName, methodName);
+ if (connector instanceof HasJavaScriptConnectorHelper) {
+ ((HasJavaScriptConnectorHelper) connector)
+ .getJavascriptConnectorHelper().invokeJsRpc(invocation,
+ parametersJson);
+ } else {
+ if (connector == null) {
+ throw new IllegalStateException("Target connector ("
+ + connector + ") not found for RCC to "
+ + getSignature(invocation));
+ }
+
+ parseMethodParameters(invocation, parametersJson, connection);
+ VConsole.log("Server to client RPC call: " + invocation);
+ applyInvocation(invocation, connector);
+ }
+ }
+
+ private void parseMethodParameters(MethodInvocation methodInvocation,
+ JSONArray parametersJson, ApplicationConnection connection) {
+ Type[] parameterTypes = getParameterTypes(methodInvocation);
+
+ Object[] parameters = new Object[parametersJson.size()];
+ for (int j = 0; j < parametersJson.size(); ++j) {
+ parameters[j] = JsonDecoder.decodeValue(parameterTypes[j],
+ parametersJson.get(j), null, connection);
+ }
+
+ methodInvocation.setParameters(parameters);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java b/client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java
new file mode 100644
index 0000000000..ce9c5b468b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/RpcMethod.java
@@ -0,0 +1,34 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+public abstract class RpcMethod {
+ private String interfaceName;
+ private String methodName;
+ private Type[] parameterTypes;
+
+ public RpcMethod(String interfaceName, String methodName,
+ Type... parameterTypes) {
+ this.interfaceName = interfaceName;
+ this.methodName = methodName;
+ this.parameterTypes = parameterTypes;
+ }
+
+ public String getInterfaceName() {
+ return interfaceName;
+ }
+
+ public String getMethodName() {
+ return methodName;
+ }
+
+ public Type[] getParameterTypes() {
+ return parameterTypes;
+ }
+
+ public abstract void applyInvocation(ClientRpc target, Object... parameters);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java b/client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java
new file mode 100644
index 0000000000..996c148f4f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/RpcProxy.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.google.gwt.core.client.GWT;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * Class for creating proxy instances for Client to Server RPC.
+ *
+ * @since 7.0
+ */
+public class RpcProxy {
+
+ private static RpcProxyCreator impl = GWT.create(RpcProxyCreator.class);
+
+ /**
+ * Create a proxy class for the given Rpc interface and assign it to the
+ * given connector.
+ *
+ * @param rpcInterface
+ * The rpc interface to construct a proxy for
+ * @param connector
+ * The connector this proxy is connected to
+ * @return A proxy class used for calling Rpc methods.
+ */
+ public static <T extends ServerRpc> T create(Class<T> rpcInterface,
+ ServerConnector connector) {
+ return impl.create(rpcInterface, connector);
+ }
+
+ public interface RpcProxyCreator {
+ <T extends ServerRpc> T create(Class<T> rpcInterface,
+ ServerConnector connector);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java b/client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java
new file mode 100644
index 0000000000..0750474d24
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/SerializerMap.java
@@ -0,0 +1,34 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.vaadin.terminal.gwt.widgetsetutils.SerializerMapGenerator;
+
+/**
+ * Provide a mapping from a type (communicated between the server and the
+ * client) and a {@link JSONSerializer} instance.
+ *
+ * An implementation of this class is created at GWT compilation time by
+ * {@link SerializerMapGenerator}, so this interface can be instantiated with
+ * GWT.create().
+ *
+ * @since 7.0
+ */
+public interface SerializerMap {
+
+ /**
+ * Returns a serializer instance for a given type.
+ *
+ * @param type
+ * type communicated on between the server and the client
+ * (currently fully qualified class name)
+ * @return serializer instance, not null
+ * @throws RuntimeException
+ * if no serializer is found
+ */
+ // TODO better error handling in javadoc and in generator
+ public JSONSerializer getSerializer(String type);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java b/client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java
new file mode 100644
index 0000000000..39ecbc022c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/StateChangeEvent.java
@@ -0,0 +1,34 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import java.io.Serializable;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler;
+
+public class StateChangeEvent extends
+ AbstractServerConnectorEvent<StateChangeHandler> {
+ /**
+ * Type of this event, used by the event bus.
+ */
+ public static final Type<StateChangeHandler> TYPE = new Type<StateChangeHandler>();
+
+ @Override
+ public Type<StateChangeHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ public StateChangeEvent() {
+ }
+
+ @Override
+ public void dispatch(StateChangeHandler listener) {
+ listener.onStateChanged(this);
+ }
+
+ public interface StateChangeHandler extends Serializable, EventHandler {
+ public void onStateChanged(StateChangeEvent stateChangeEvent);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/Type.java b/client/src/com/vaadin/terminal/gwt/client/communication/Type.java
new file mode 100644
index 0000000000..dc33f760ff
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/Type.java
@@ -0,0 +1,40 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+public class Type {
+ private final String baseTypeName;
+ private final Type[] parameterTypes;
+
+ public Type(String baseTypeName, Type[] parameterTypes) {
+ this.baseTypeName = baseTypeName;
+ this.parameterTypes = parameterTypes;
+ }
+
+ public String getBaseTypeName() {
+ return baseTypeName;
+ }
+
+ public Type[] getParameterTypes() {
+ return parameterTypes;
+ }
+
+ @Override
+ public String toString() {
+ String string = baseTypeName;
+ if (parameterTypes != null) {
+ string += '<';
+ for (int i = 0; i < parameterTypes.length; i++) {
+ if (i != 0) {
+ string += ',';
+ }
+ string += parameterTypes[i].toString();
+ }
+ string += '>';
+ }
+
+ return string;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java b/client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java
new file mode 100644
index 0000000000..f39cad1899
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/communication/URLReference_Serializer.java
@@ -0,0 +1,41 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.communication;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+import com.vaadin.shared.communication.URLReference;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+public class URLReference_Serializer implements JSONSerializer<URLReference> {
+
+ // setURL() -> uRL as first char becomes lower case...
+ private static final String URL_FIELD = "uRL";
+
+ @Override
+ public URLReference deserialize(Type type, JSONValue jsonValue,
+ ApplicationConnection connection) {
+ URLReference reference = GWT.create(URLReference.class);
+ JSONObject json = (JSONObject) jsonValue;
+ if (json.containsKey(URL_FIELD)) {
+ JSONValue jsonURL = json.get(URL_FIELD);
+ String URL = (String) JsonDecoder.decodeValue(
+ new Type(String.class.getName(), null), jsonURL, null,
+ connection);
+ reference.setURL(connection.translateVaadinUri(URL));
+ }
+ return reference;
+ }
+
+ @Override
+ public JSONValue serialize(URLReference value,
+ ApplicationConnection connection) {
+ JSONObject json = new JSONObject();
+ json.put(URL_FIELD,
+ JsonEncoder.encode(value.getURL(), true, connection));
+ return json;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java b/client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java
new file mode 100644
index 0000000000..408f03f6cb
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/extensions/AbstractExtensionConnector.java
@@ -0,0 +1,36 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.extensions;
+
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.ui.AbstractConnector;
+
+public abstract class AbstractExtensionConnector extends AbstractConnector {
+ boolean hasBeenAttached = false;
+
+ @Override
+ public void setParent(ServerConnector parent) {
+ ServerConnector oldParent = getParent();
+ if (oldParent == parent) {
+ // Nothing to do
+ return;
+ }
+ if (hasBeenAttached && parent != null) {
+ throw new IllegalStateException(
+ "An extension can not be moved from one parent to another.");
+ }
+
+ super.setParent(parent);
+
+ if (parent != null) {
+ extend(parent);
+ hasBeenAttached = true;
+ }
+ }
+
+ protected void extend(ServerConnector target) {
+ // Default does nothing
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java b/client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java
new file mode 100644
index 0000000000..d5849096fa
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/extensions/javascriptmanager/JavaScriptManagerConnector.java
@@ -0,0 +1,122 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.extensions.javascriptmanager;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.json.client.JSONArray;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc;
+import com.vaadin.shared.extension.javascriptmanager.JavaScriptManagerState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.extensions.AbstractExtensionConnector;
+import com.vaadin.ui.JavaScript;
+
+@Connect(JavaScript.class)
+public class JavaScriptManagerConnector extends AbstractExtensionConnector {
+ private Set<String> currentNames = new HashSet<String>();
+
+ @Override
+ protected void init() {
+ registerRpc(ExecuteJavaScriptRpc.class, new ExecuteJavaScriptRpc() {
+ @Override
+ public void executeJavaScript(String Script) {
+ eval(Script);
+ }
+ });
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ Set<String> newNames = getState().getNames();
+
+ // Current names now only contains orphan callbacks
+ currentNames.removeAll(newNames);
+
+ for (String name : currentNames) {
+ removeCallback(name);
+ }
+
+ currentNames = new HashSet<String>(newNames);
+ for (String name : newNames) {
+ addCallback(name);
+ }
+ }
+
+ // TODO Ensure we don't overwrite anything (important) in $wnd
+ private native void addCallback(String name)
+ /*-{
+ var m = this;
+ var target = $wnd;
+ var parts = name.split('.');
+
+ for(var i = 0; i < parts.length - 1; i++) {
+ var part = parts[i];
+ if (target[part] === undefined) {
+ target[part] = {};
+ }
+ target = target[part];
+ }
+
+ target[parts[parts.length - 1]] = $entry(function() {
+ //Must make a copy because arguments is an array-like object (not instanceof Array), causing suboptimal JSON encoding
+ var args = Array.prototype.slice.call(arguments, 0);
+ m.@com.vaadin.terminal.gwt.client.extensions.javascriptmanager.JavaScriptManagerConnector::sendRpc(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;)(name, args);
+ });
+ }-*/;
+
+ // TODO only remove what we actually added
+ // TODO We might leave empty objects behind, but there's no good way of
+ // knowing whether they are unused
+ private native void removeCallback(String name)
+ /*-{
+ var target = $wnd;
+ var parts = name.split('.');
+
+ for(var i = 0; i < parts.length - 1; i++) {
+ var part = parts[i];
+ if (target[part] === undefined) {
+ $wnd.console.log(part,'not defined in',target);
+ // No longer attached -> nothing more to do
+ return;
+ }
+ target = target[part];
+ }
+
+ $wnd.console.log('removing',parts[parts.length - 1],'from',target);
+ delete target[parts[parts.length - 1]];
+ }-*/;
+
+ private static native void eval(String script)
+ /*-{
+ if(script) {
+ (new $wnd.Function(script)).apply($wnd);
+ }
+ }-*/;
+
+ public void sendRpc(String name, JsArray<JavaScriptObject> arguments) {
+ Object[] parameters = new Object[] { name, new JSONArray(arguments) };
+
+ /*
+ * Must invoke manually as the RPC interface can't be used in GWT
+ * because of the JSONArray parameter
+ */
+ getConnection().addMethodInvocationToQueue(
+ new MethodInvocation(getConnectorId(),
+ "com.vaadin.ui.JavaScript$JavaScriptCallbackRpc",
+ "call", parameters), true);
+ }
+
+ @Override
+ public JavaScriptManagerState getState() {
+ return (JavaScriptManagerState) super.getState();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java
new file mode 100644
index 0000000000..9de465e4a5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractClickEventHandler.java
@@ -0,0 +1,234 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.Util;
+
+public abstract class AbstractClickEventHandler implements MouseDownHandler,
+ MouseUpHandler, DoubleClickHandler, ContextMenuHandler {
+
+ private HandlerRegistration mouseDownHandlerRegistration;
+ private HandlerRegistration mouseUpHandlerRegistration;
+ private HandlerRegistration doubleClickHandlerRegistration;
+ private HandlerRegistration contextMenuHandlerRegistration;
+
+ protected ComponentConnector connector;
+ private String clickEventIdentifier;
+
+ /**
+ * The element where the last mouse down event was registered.
+ */
+ private JavaScriptObject lastMouseDownTarget;
+
+ /**
+ * Set to true by {@link #mouseUpPreviewHandler} if it gets a mouseup at the
+ * same element as {@link #lastMouseDownTarget}.
+ */
+ private boolean mouseUpPreviewMatched = false;
+
+ private HandlerRegistration mouseUpEventPreviewRegistration;
+
+ /**
+ * Previews events after a mousedown to detect where the following mouseup
+ * hits.
+ */
+ private final NativePreviewHandler mouseUpPreviewHandler = new NativePreviewHandler() {
+
+ @Override
+ public void onPreviewNativeEvent(NativePreviewEvent event) {
+ if (event.getTypeInt() == Event.ONMOUSEUP) {
+ mouseUpEventPreviewRegistration.removeHandler();
+
+ // Event's reported target not always correct if event
+ // capture is in use
+ Element elementUnderMouse = Util.getElementUnderMouse(event
+ .getNativeEvent());
+ if (lastMouseDownTarget != null
+ && elementUnderMouse.cast() == lastMouseDownTarget) {
+ mouseUpPreviewMatched = true;
+ } else {
+ System.out.println("Ignoring mouseup from "
+ + elementUnderMouse + " when mousedown was on "
+ + lastMouseDownTarget);
+ }
+ }
+ }
+ };
+
+ public AbstractClickEventHandler(ComponentConnector connector,
+ String clickEventIdentifier) {
+ this.connector = connector;
+ this.clickEventIdentifier = clickEventIdentifier;
+ }
+
+ public void handleEventHandlerRegistration() {
+ // Handle registering/unregistering of click handler depending on if
+ // server side listeners have been added or removed.
+ if (hasEventListener()) {
+ if (mouseDownHandlerRegistration == null) {
+ mouseDownHandlerRegistration = registerHandler(this,
+ MouseDownEvent.getType());
+ mouseUpHandlerRegistration = registerHandler(this,
+ MouseUpEvent.getType());
+ doubleClickHandlerRegistration = registerHandler(this,
+ DoubleClickEvent.getType());
+ contextMenuHandlerRegistration = registerHandler(this,
+ ContextMenuEvent.getType());
+ }
+ } else {
+ if (mouseDownHandlerRegistration != null) {
+ // Remove existing handlers
+ mouseDownHandlerRegistration.removeHandler();
+ mouseUpHandlerRegistration.removeHandler();
+ doubleClickHandlerRegistration.removeHandler();
+ contextMenuHandlerRegistration.removeHandler();
+
+ mouseDownHandlerRegistration = null;
+ mouseUpHandlerRegistration = null;
+ doubleClickHandlerRegistration = null;
+ contextMenuHandlerRegistration = null;
+ }
+ }
+
+ }
+
+ /**
+ * Registers the given handler to the widget so that the necessary events
+ * are passed to this {@link ClickEventHandler}.
+ * <p>
+ * By default registers the handler with the connector root widget.
+ * </p>
+ *
+ * @param <H>
+ * @param handler
+ * The handler to register
+ * @param type
+ * The type of the handler.
+ * @return A reference for the registration of the handler.
+ */
+ protected <H extends EventHandler> HandlerRegistration registerHandler(
+ final H handler, DomEvent.Type<H> type) {
+ return connector.getWidget().addDomHandler(handler, type);
+ }
+
+ /**
+ * Checks if there is a server side event listener registered for clicks
+ *
+ * @return true if there is a server side event listener registered, false
+ * otherwise
+ */
+ public boolean hasEventListener() {
+ return connector.hasEventListener(clickEventIdentifier);
+ }
+
+ /**
+ * Event handler for context menu. Prevents the browser context menu from
+ * popping up if there is a listener for right clicks.
+ */
+
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ if (hasEventListener() && shouldFireEvent(event)) {
+ // Prevent showing the browser's context menu when there is a right
+ // click listener.
+ event.preventDefault();
+ }
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ /*
+ * When getting a mousedown event, we must detect where the
+ * corresponding mouseup event if it's on a different part of the page.
+ */
+ lastMouseDownTarget = event.getNativeEvent().getEventTarget();
+ mouseUpPreviewMatched = false;
+ mouseUpEventPreviewRegistration = Event
+ .addNativePreviewHandler(mouseUpPreviewHandler);
+ }
+
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ /*
+ * Only fire a click if the mouseup hits the same element as the
+ * corresponding mousedown. This is first checked in the event preview
+ * but we can't fire the even there as the event might get canceled
+ * before it gets here.
+ */
+ if (hasEventListener()
+ && mouseUpPreviewMatched
+ && lastMouseDownTarget != null
+ && Util.getElementUnderMouse(event.getNativeEvent()) == lastMouseDownTarget
+ && shouldFireEvent(event)) {
+ // "Click" with left, right or middle button
+ fireClick(event.getNativeEvent());
+ }
+ mouseUpPreviewMatched = false;
+ lastMouseDownTarget = null;
+ }
+
+ /**
+ * Sends the click event based on the given native event.
+ *
+ * @param event
+ * The native event that caused this click event
+ */
+ protected abstract void fireClick(NativeEvent event);
+
+ /**
+ * Called before firing a click event. Allows sub classes to decide if this
+ * in an event that should cause an event or not.
+ *
+ * @param event
+ * The user event
+ * @return true if the event should be fired, false otherwise
+ */
+ protected boolean shouldFireEvent(DomEvent<?> event) {
+ return true;
+ }
+
+ /**
+ * Event handler for double clicks. Used to fire double click events. Note
+ * that browsers typically fail to prevent the second click event so a
+ * double click will result in two click events and one double click event.
+ */
+
+ @Override
+ public void onDoubleClick(DoubleClickEvent event) {
+ if (hasEventListener() && shouldFireEvent(event)) {
+ fireClick(event.getNativeEvent());
+ }
+ }
+
+ /**
+ * Click event calculates and returns coordinates relative to the element
+ * returned by this method. Default implementation uses the root element of
+ * the widget. Override to provide a different relative element.
+ *
+ * @return The Element used for calculating relative coordinates for a click
+ * or null if no relative coordinates can be calculated.
+ */
+ protected Element getRelativeToElement() {
+ return connector.getWidget().getElement();
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java
new file mode 100644
index 0000000000..ba3a53691a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentConnector.java
@@ -0,0 +1,407 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.HasEnabled;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.ui.TabIndexState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.datefield.PopupDateFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.root.RootConnector;
+
+public abstract class AbstractComponentConnector extends AbstractConnector
+ implements ComponentConnector {
+
+ private Widget widget;
+
+ private String lastKnownWidth = "";
+ private String lastKnownHeight = "";
+
+ /**
+ * The style names from getState().getStyles() which are currently applied
+ * to the widget.
+ */
+ protected List<String> styleNames = new ArrayList<String>();
+
+ /**
+ * Default constructor
+ */
+ public AbstractComponentConnector() {
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+
+ getConnection().getVTooltip().connectHandlersToWidget(getWidget());
+
+ // Set v-connector style names for the widget
+ getWidget().setStyleName("v-connector", true);
+ }
+
+ /**
+ * Creates and returns the widget for this VPaintableWidget. This method
+ * should only be called once when initializing the paintable.
+ *
+ * @return
+ */
+ protected Widget createWidget() {
+ return ConnectorWidgetFactory.createWidget(getClass());
+ }
+
+ /**
+ * Returns the widget associated with this paintable. The widget returned by
+ * this method must not changed during the life time of the paintable.
+ *
+ * @return The widget associated with this paintable
+ */
+ @Override
+ public Widget getWidget() {
+ if (widget == null) {
+ widget = createWidget();
+ }
+
+ return widget;
+ }
+
+ @Deprecated
+ public static boolean isRealUpdate(UIDL uidl) {
+ return !uidl.hasAttribute("cached");
+ }
+
+ @Override
+ public ComponentState getState() {
+ return (ComponentState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ ConnectorMap paintableMap = ConnectorMap.get(getConnection());
+
+ if (getState().getDebugId() != null) {
+ getWidget().getElement().setId(getState().getDebugId());
+ } else {
+ getWidget().getElement().removeAttribute("id");
+
+ }
+
+ /*
+ * Disabled state may affect (override) tabindex so the order must be
+ * first setting tabindex, then enabled state (through super
+ * implementation).
+ */
+ if (getState() instanceof TabIndexState
+ && getWidget() instanceof Focusable) {
+ ((Focusable) getWidget()).setTabIndex(((TabIndexState) getState())
+ .getTabIndex());
+ }
+
+ super.onStateChanged(stateChangeEvent);
+
+ // Style names
+ updateWidgetStyleNames();
+
+ // Set captions
+ if (delegateCaptionHandling()) {
+ ServerConnector parent = getParent();
+ if (parent instanceof ComponentContainerConnector) {
+ ((ComponentContainerConnector) parent).updateCaption(this);
+ } else if (parent == null && !(this instanceof RootConnector)) {
+ VConsole.error("Parent of connector "
+ + Util.getConnectorString(this)
+ + " is null. This is typically an indication of a broken component hierarchy");
+ }
+ }
+
+ /*
+ * updateComponentSize need to be after caption update so caption can be
+ * taken into account
+ */
+
+ updateComponentSize();
+ }
+
+ @Override
+ public void setWidgetEnabled(boolean widgetEnabled) {
+ // add or remove v-disabled style name from the widget
+ setWidgetStyleName(ApplicationConnection.DISABLED_CLASSNAME,
+ !widgetEnabled);
+
+ if (getWidget() instanceof HasEnabled) {
+ // set widget specific enabled state
+ ((HasEnabled) getWidget()).setEnabled(widgetEnabled);
+
+ // make sure the caption has or has not v-disabled style
+ if (delegateCaptionHandling()) {
+ ServerConnector parent = getParent();
+ if (parent instanceof ComponentContainerConnector) {
+ ((ComponentContainerConnector) parent).updateCaption(this);
+ } else if (parent == null && !(this instanceof RootConnector)) {
+ VConsole.error("Parent of connector "
+ + Util.getConnectorString(this)
+ + " is null. This is typically an indication of a broken component hierarchy");
+ }
+ }
+ }
+ }
+
+ private void updateComponentSize() {
+ String newWidth = getState().getWidth();
+ String newHeight = getState().getHeight();
+
+ // Parent should be updated if either dimension changed between relative
+ // and non-relative
+ if (newWidth.endsWith("%") != lastKnownWidth.endsWith("%")) {
+ Connector parent = getParent();
+ if (parent instanceof ManagedLayout) {
+ getLayoutManager().setNeedsHorizontalLayout(
+ (ManagedLayout) parent);
+ }
+ }
+
+ if (newHeight.endsWith("%") != lastKnownHeight.endsWith("%")) {
+ Connector parent = getParent();
+ if (parent instanceof ManagedLayout) {
+ getLayoutManager().setNeedsVerticalLayout(
+ (ManagedLayout) parent);
+ }
+ }
+
+ lastKnownWidth = newWidth;
+ lastKnownHeight = newHeight;
+
+ // Set defined sizes
+ Widget widget = getWidget();
+
+ widget.setStyleName("v-has-width", !isUndefinedWidth());
+ widget.setStyleName("v-has-height", !isUndefinedHeight());
+
+ widget.setHeight(newHeight);
+ widget.setWidth(newWidth);
+ }
+
+ @Override
+ public boolean isRelativeHeight() {
+ return getState().getHeight().endsWith("%");
+ }
+
+ @Override
+ public boolean isRelativeWidth() {
+ return getState().getWidth().endsWith("%");
+ }
+
+ @Override
+ public boolean isUndefinedHeight() {
+ return getState().getHeight().length() == 0;
+ }
+
+ @Override
+ public boolean isUndefinedWidth() {
+ return getState().getWidth().length() == 0;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ComponentConnector#delegateCaptionHandling
+ * ()
+ */
+ @Override
+ public boolean delegateCaptionHandling() {
+ return true;
+ }
+
+ /**
+ * Updates the user defined, read-only and error style names for the widget
+ * based the shared state. User defined style names are prefixed with the
+ * primary style name of the widget returned by {@link #getWidget()}
+ * <p>
+ * This method can be overridden to provide additional style names for the
+ * component, for example see
+ * {@link AbstractFieldConnector#updateWidgetStyleNames()}
+ * </p>
+ */
+ protected void updateWidgetStyleNames() {
+ ComponentState state = getState();
+
+ String primaryStyleName = getWidget().getStylePrimaryName();
+
+ // should be in AbstractFieldConnector ?
+ // add / remove read-only style name
+ setWidgetStyleName("v-readonly", isReadOnly());
+
+ // add / remove error style name
+ setWidgetStyleNameWithPrefix(primaryStyleName,
+ ApplicationConnection.ERROR_CLASSNAME_EXT,
+ null != state.getErrorMessage());
+
+ // add additional user defined style names as class names, prefixed with
+ // component default class name. remove nonexistent style names.
+ if (state.hasStyles()) {
+ // add new style names
+ List<String> newStyles = new ArrayList<String>();
+ newStyles.addAll(state.getStyles());
+ newStyles.removeAll(styleNames);
+ for (String newStyle : newStyles) {
+ setWidgetStyleName(newStyle, true);
+ setWidgetStyleNameWithPrefix(primaryStyleName + "-", newStyle,
+ true);
+ }
+ // remove nonexistent style names
+ styleNames.removeAll(state.getStyles());
+ for (String oldStyle : styleNames) {
+ setWidgetStyleName(oldStyle, false);
+ setWidgetStyleNameWithPrefix(primaryStyleName + "-", oldStyle,
+ false);
+ }
+ styleNames.clear();
+ styleNames.addAll(state.getStyles());
+ } else {
+ // remove all old style names
+ for (String oldStyle : styleNames) {
+ setWidgetStyleName(oldStyle, false);
+ setWidgetStyleNameWithPrefix(primaryStyleName + "-", oldStyle,
+ false);
+ }
+ styleNames.clear();
+ }
+
+ }
+
+ /**
+ * This is used to add / remove state related style names from the widget.
+ * <p>
+ * Override this method for example if the style name given here should be
+ * updated in another widget in addition to the one returned by the
+ * {@link #getWidget()}.
+ * </p>
+ *
+ * @param styleName
+ * the style name to be added or removed
+ * @param add
+ * <code>true</code> to add the given style, <code>false</code>
+ * to remove it
+ */
+ protected void setWidgetStyleName(String styleName, boolean add) {
+ getWidget().setStyleName(styleName, add);
+ }
+
+ /**
+ * This is used to add / remove state related prefixed style names from the
+ * widget.
+ * <p>
+ * Override this method if the prefixed style name given here should be
+ * updated in another widget in addition to the one returned by the
+ * <code>Connector</code>'s {@link #getWidget()}, or if the prefix should be
+ * different. For example see
+ * {@link PopupDateFieldConnector#setWidgetStyleNameWithPrefix(String, String, boolean)}
+ * </p>
+ *
+ * @param styleName
+ * the style name to be added or removed
+ * @param add
+ * <code>true</code> to add the given style, <code>false</code>
+ * to remove it
+ * @deprecated This will be removed once styles are no longer added with
+ * prefixes.
+ */
+ @Deprecated
+ protected void setWidgetStyleNameWithPrefix(String prefix,
+ String styleName, boolean add) {
+ if (!styleName.startsWith("-")) {
+ if (!prefix.endsWith("-")) {
+ prefix += "-";
+ }
+ } else {
+ if (prefix.endsWith("-")) {
+ styleName.replaceFirst("-", "");
+ }
+ }
+ getWidget().setStyleName(prefix + styleName, add);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ComponentConnector#isReadOnly()
+ */
+ @Override
+ @Deprecated
+ public boolean isReadOnly() {
+ return getState().isReadOnly();
+ }
+
+ @Override
+ public LayoutManager getLayoutManager() {
+ return LayoutManager.get(getConnection());
+ }
+
+ /**
+ * Checks if there is a registered server side listener for the given event
+ * identifier.
+ *
+ * @param eventIdentifier
+ * The identifier to check for
+ * @return true if an event listener has been registered with the given
+ * event identifier on the server side, false otherwise
+ */
+ @Override
+ public boolean hasEventListener(String eventIdentifier) {
+ Set<String> reg = getState().getRegisteredEventListeners();
+ return (reg != null && reg.contains(eventIdentifier));
+ }
+
+ @Override
+ public void updateEnabledState(boolean enabledState) {
+ super.updateEnabledState(enabledState);
+
+ setWidgetEnabled(isEnabled());
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+
+ // Show an error if widget is still attached to DOM. It should never be
+ // at this point.
+ if (getWidget() != null && getWidget().isAttached()) {
+ getWidget().removeFromParent();
+ VConsole.error("Widget is still attached to the DOM after the connector ("
+ + Util.getConnectorString(this)
+ + ") has been unregistered. Widget was removed.");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ComponentConnector#getTooltipInfo(com.
+ * google.gwt.dom.client.Element)
+ */
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+ return new TooltipInfo(getState().getDescription(), getState()
+ .getErrorMessage());
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java
new file mode 100644
index 0000000000..6b294e65b8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractComponentContainerConnector.java
@@ -0,0 +1,91 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent.ConnectorHierarchyChangeHandler;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+
+public abstract class AbstractComponentContainerConnector extends
+ AbstractComponentConnector implements ComponentContainerConnector,
+ ConnectorHierarchyChangeHandler {
+
+ List<ComponentConnector> childComponents;
+
+ private final boolean debugLogging = false;
+
+ /**
+ * Default constructor
+ */
+ public AbstractComponentContainerConnector() {
+ addConnectorHierarchyChangeHandler(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ComponentContainerConnector#getChildren()
+ */
+ @Override
+ public List<ComponentConnector> getChildComponents() {
+ if (childComponents == null) {
+ return Collections.emptyList();
+ }
+
+ return childComponents;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ComponentContainerConnector#setChildren
+ * (java.util.Collection)
+ */
+ @Override
+ public void setChildComponents(List<ComponentConnector> childComponents) {
+ this.childComponents = childComponents;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ComponentContainerConnector#
+ * connectorHierarchyChanged
+ * (com.vaadin.terminal.gwt.client.ConnectorHierarchyChangedEvent)
+ */
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ if (debugLogging) {
+ VConsole.log("Hierarchy changed for "
+ + Util.getConnectorString(this));
+ String oldChildren = "* Old children: ";
+ for (ComponentConnector child : event.getOldChildren()) {
+ oldChildren += Util.getConnectorString(child) + " ";
+ }
+ VConsole.log(oldChildren);
+
+ String newChildren = "* New children: ";
+ for (ComponentConnector child : getChildComponents()) {
+ newChildren += Util.getConnectorString(child) + " ";
+ }
+ VConsole.log(newChildren);
+ }
+ }
+
+ @Override
+ public HandlerRegistration addConnectorHierarchyChangeHandler(
+ ConnectorHierarchyChangeHandler handler) {
+ return ensureHandlerManager().addHandler(
+ ConnectorHierarchyChangeEvent.TYPE, handler);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java
new file mode 100644
index 0000000000..1a504b7a75
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java
@@ -0,0 +1,278 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler;
+
+/**
+ * An abstract implementation of Connector.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public abstract class AbstractConnector implements ServerConnector,
+ StateChangeHandler {
+
+ private ApplicationConnection connection;
+ private String id;
+
+ private HandlerManager handlerManager;
+ private Map<String, Collection<ClientRpc>> rpcImplementations;
+ private final boolean debugLogging = false;
+
+ private SharedState state;
+ private ServerConnector parent;
+
+ /**
+ * Temporary storage for last enabled state to be able to see if it has
+ * changed. Can be removed once we are able to listen specifically for
+ * enabled changes in the state. Widget.isEnabled() cannot be used as all
+ * Widgets do not implement HasEnabled
+ */
+ private boolean lastEnabledState = true;
+ private List<ServerConnector> children;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.VPaintable#getConnection()
+ */
+ @Override
+ public final ApplicationConnection getConnection() {
+ return connection;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Connector#getId()
+ */
+ @Override
+ public String getConnectorId() {
+ return id;
+ }
+
+ /**
+ * Called once by the framework to initialize the connector.
+ * <p>
+ * Note that the shared state is not yet available when this method is
+ * called.
+ * <p>
+ * Connector classes should override {@link #init()} instead of this method.
+ */
+ @Override
+ public final void doInit(String connectorId,
+ ApplicationConnection connection) {
+ this.connection = connection;
+ id = connectorId;
+
+ addStateChangeHandler(this);
+ init();
+ }
+
+ /**
+ * Called when the connector has been initialized. Override this method to
+ * perform initialization of the connector.
+ */
+ // FIXME: It might make sense to make this abstract to force users to
+ // use init instead of constructor, where connection and id has not yet been
+ // set.
+ protected void init() {
+
+ }
+
+ /**
+ * Registers an implementation for a server to client RPC interface.
+ *
+ * Multiple registrations can be made for a single interface, in which case
+ * all of them receive corresponding RPC calls.
+ *
+ * @param rpcInterface
+ * RPC interface
+ * @param implementation
+ * implementation that should receive RPC calls
+ * @param <T>
+ * The type of the RPC interface that is being registered
+ */
+ protected <T extends ClientRpc> void registerRpc(Class<T> rpcInterface,
+ T implementation) {
+ String rpcInterfaceId = rpcInterface.getName().replaceAll("\\$", ".");
+ if (null == rpcImplementations) {
+ rpcImplementations = new HashMap<String, Collection<ClientRpc>>();
+ }
+ if (null == rpcImplementations.get(rpcInterfaceId)) {
+ rpcImplementations.put(rpcInterfaceId, new ArrayList<ClientRpc>());
+ }
+ rpcImplementations.get(rpcInterfaceId).add(implementation);
+ }
+
+ /**
+ * Unregisters an implementation for a server to client RPC interface.
+ *
+ * @param rpcInterface
+ * RPC interface
+ * @param implementation
+ * implementation to unregister
+ */
+ protected <T extends ClientRpc> void unregisterRpc(Class<T> rpcInterface,
+ T implementation) {
+ String rpcInterfaceId = rpcInterface.getName().replaceAll("\\$", ".");
+ if (null != rpcImplementations
+ && null != rpcImplementations.get(rpcInterfaceId)) {
+ rpcImplementations.get(rpcInterfaceId).remove(implementation);
+ }
+ }
+
+ @Override
+ public <T extends ClientRpc> Collection<T> getRpcImplementations(
+ String rpcInterfaceId) {
+ if (null == rpcImplementations) {
+ return Collections.emptyList();
+ }
+ return (Collection<T>) rpcImplementations.get(rpcInterfaceId);
+ }
+
+ @Override
+ public void fireEvent(GwtEvent<?> event) {
+ if (handlerManager != null) {
+ handlerManager.fireEvent(event);
+ }
+ }
+
+ protected HandlerManager ensureHandlerManager() {
+ if (handlerManager == null) {
+ handlerManager = new HandlerManager(this);
+ }
+
+ return handlerManager;
+ }
+
+ @Override
+ public HandlerRegistration addStateChangeHandler(StateChangeHandler handler) {
+ return ensureHandlerManager()
+ .addHandler(StateChangeEvent.TYPE, handler);
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ if (debugLogging) {
+ VConsole.log("State change event for "
+ + Util.getConnectorString(stateChangeEvent.getConnector())
+ + " received by " + Util.getConnectorString(this));
+ }
+
+ updateEnabledState(isEnabled());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ServerConnector#onUnregister()
+ */
+ @Override
+ public void onUnregister() {
+ if (debugLogging) {
+ VConsole.log("Unregistered connector "
+ + Util.getConnectorString(this));
+ }
+
+ }
+
+ /**
+ * Returns the shared state object for this connector.
+ *
+ * Override this method to define the shared state type for your connector.
+ *
+ * @return the current shared state (never null)
+ */
+ @Override
+ public SharedState getState() {
+ if (state == null) {
+ state = createState();
+ }
+
+ return state;
+ }
+
+ /**
+ * Creates a state object with default values for this connector. The
+ * created state object must be compatible with the return type of
+ * {@link #getState()}. The default implementation creates a state object
+ * using GWT.create() using the defined return type of {@link #getState()}.
+ *
+ * @return A new state object
+ */
+ protected SharedState createState() {
+ return ConnectorStateFactory.createState(getClass());
+ }
+
+ @Override
+ public ServerConnector getParent() {
+ return parent;
+ }
+
+ @Override
+ public void setParent(ServerConnector parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public List<ServerConnector> getChildren() {
+ if (children == null) {
+ return Collections.emptyList();
+ }
+ return children;
+ }
+
+ @Override
+ public void setChildren(List<ServerConnector> children) {
+ this.children = children;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ if (!getState().isEnabled()) {
+ return false;
+ }
+
+ if (getParent() == null) {
+ return true;
+ } else {
+ return getParent().isEnabled();
+ }
+ }
+
+ @Override
+ public void updateEnabledState(boolean enabledState) {
+ if (lastEnabledState == enabledState) {
+ return;
+ }
+ lastEnabledState = enabledState;
+
+ for (ServerConnector c : getChildren()) {
+ // Update children as they might be affected by the enabled state of
+ // their parent
+ c.updateEnabledState(c.isEnabled());
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java
new file mode 100644
index 0000000000..4611b5a4ed
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractFieldConnector.java
@@ -0,0 +1,49 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+public abstract class AbstractFieldConnector extends AbstractComponentConnector {
+
+ @Override
+ public AbstractFieldState getState() {
+ return (AbstractFieldState) super.getState();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().isPropertyReadOnly();
+ }
+
+ public boolean isModified() {
+ return getState().isModified();
+ }
+
+ /**
+ * Checks whether the required indicator should be shown for the field.
+ *
+ * Required indicators are hidden if the field or its data source is
+ * read-only.
+ *
+ * @return true if required indicator should be shown
+ */
+ public boolean isRequired() {
+ return getState().isRequired() && !isReadOnly();
+ }
+
+ @Override
+ protected void updateWidgetStyleNames() {
+ super.updateWidgetStyleNames();
+
+ // add / remove modified style name to Fields
+ setWidgetStyleName(ApplicationConnection.MODIFIED_CLASSNAME,
+ isModified());
+
+ // add / remove error style name to Fields
+ setWidgetStyleNameWithPrefix(getWidget().getStylePrimaryName(),
+ ApplicationConnection.REQUIRED_CLASSNAME_EXT, isRequired());
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java
new file mode 100644
index 0000000000..cd059357a8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/AbstractLayoutConnector.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.shared.ui.AbstractLayoutState;
+
+public abstract class AbstractLayoutConnector extends
+ AbstractComponentContainerConnector {
+
+ @Override
+ public AbstractLayoutState getState() {
+ return (AbstractLayoutState) super.getState();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Action.java b/client/src/com/vaadin/terminal/gwt/client/ui/Action.java
new file mode 100644
index 0000000000..1a92e30f3f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/Action.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Command;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ *
+ */
+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
+ */
+ @Override
+ public abstract void execute();
+
+ public String getHTML() {
+ final StringBuffer sb = new StringBuffer();
+ sb.append("<div>");
+ if (getIconUrl() != null) {
+ sb.append("<img src=\"" + Util.escapeAttribute(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/client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java b/client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java
new file mode 100644
index 0000000000..5e0f431f49
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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/client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java b/client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java
new file mode 100644
index 0000000000..1577d60ab7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java
@@ -0,0 +1,128 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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;
+
+ @SuppressWarnings("deprecation")
+ 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;
+ }
+
+ @SuppressWarnings("deprecation")
+ 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/client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java
new file mode 100644
index 0000000000..b7b6b13d3c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ClickEventHandler.java
@@ -0,0 +1,51 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+
+public abstract class ClickEventHandler extends AbstractClickEventHandler {
+
+ public static final String CLICK_EVENT_IDENTIFIER = "click";
+
+ public ClickEventHandler(ComponentConnector connector) {
+ this(connector, CLICK_EVENT_IDENTIFIER);
+ }
+
+ public ClickEventHandler(ComponentConnector connector,
+ String clickEventIdentifier) {
+ super(connector, clickEventIdentifier);
+ }
+
+ /**
+ * Sends the click event based on the given native event. Delegates actual
+ * sending to {@link #fireClick(MouseEventDetails)}.
+ *
+ * @param event
+ * The native event that caused this click event
+ */
+ @Override
+ protected void fireClick(NativeEvent event) {
+ MouseEventDetails mouseDetails = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event, getRelativeToElement());
+ fireClick(event, mouseDetails);
+ }
+
+ /**
+ * Sends the click event to the server. Must be implemented by sub classes,
+ * typically by calling an RPC method.
+ *
+ * @param event
+ * The event that caused this click to be fired
+ *
+ * @param mouseDetails
+ * The mouse details for the event
+ */
+ protected abstract void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails);
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java
new file mode 100644
index 0000000000..79dc138df9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorClassBasedFactory.java
@@ -0,0 +1,40 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.shared.Connector;
+
+public abstract class ConnectorClassBasedFactory<T> {
+ public interface Creator<T> {
+ public T create();
+ }
+
+ private Map<Class<? extends Connector>, Creator<? extends T>> creators = new HashMap<Class<? extends Connector>, Creator<? extends T>>();
+
+ protected void addCreator(Class<? extends Connector> cls,
+ Creator<? extends T> creator) {
+ creators.put(cls, creator);
+ }
+
+ /**
+ * Creates a widget using GWT.create for the given connector, based on its
+ * {@link AbstractComponentConnector#getWidget()} return type.
+ *
+ * @param connector
+ * @return
+ */
+ public T create(Class<? extends Connector> connector) {
+ Creator<? extends T> foo = creators.get(connector);
+ if (foo == null) {
+ throw new RuntimeException(getClass().getName()
+ + " could not find a creator for connector of type "
+ + connector.getName());
+ }
+ return foo.create();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java
new file mode 100644
index 0000000000..535fd29dfe
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorStateFactory.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.core.client.GWT;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.communication.SharedState;
+
+public abstract class ConnectorStateFactory extends
+ ConnectorClassBasedFactory<SharedState> {
+ private static ConnectorStateFactory impl = null;
+
+ /**
+ * Creates a SharedState using GWT.create for the given connector, based on
+ * its {@link AbstractComponentConnector#getSharedState ()} return type.
+ *
+ * @param connector
+ * @return
+ */
+ public static SharedState createState(Class<? extends Connector> connector) {
+ return getImpl().create(connector);
+ }
+
+ private static ConnectorStateFactory getImpl() {
+ if (impl == null) {
+ impl = GWT.create(ConnectorStateFactory.class);
+ }
+ return impl;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java
new file mode 100644
index 0000000000..03d2069a94
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ConnectorWidgetFactory.java
@@ -0,0 +1,43 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ui.textfield.TextFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+public abstract class ConnectorWidgetFactory extends
+ ConnectorClassBasedFactory<Widget> {
+ private static ConnectorWidgetFactory impl = null;
+
+ // TODO Move to generator
+ {
+ addCreator(TextFieldConnector.class, new Creator<Widget>() {
+ @Override
+ public Widget create() {
+ return GWT.create(VTextField.class);
+ }
+ });
+ }
+
+ /**
+ * Creates a widget using GWT.create for the given connector, based on its
+ * {@link AbstractComponentConnector#getWidget()} return type.
+ *
+ * @param connector
+ * @return
+ */
+ public static Widget createWidget(
+ Class<? extends AbstractComponentConnector> connector) {
+ return getImpl().create(connector);
+ }
+
+ private static ConnectorWidgetFactory getImpl() {
+ if (impl == null) {
+ impl = GWT.create(ConnectorWidgetFactory.class);
+ }
+ return impl;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Field.java b/client/src/com/vaadin/terminal/gwt/client/ui/Field.java
new file mode 100644
index 0000000000..010892b0e9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/Field.java
@@ -0,0 +1,16 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+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 v-modified classname.
+ *
+ */
+public interface Field {
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java
new file mode 100644
index 0000000000..4984c4ce3b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusElementPanel.java
@@ -0,0 +1,80 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+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.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+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.Widget;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+
+/**
+ * A panel that contains an always visible 0x0 size element that holds the focus
+ */
+public class FocusElementPanel extends SimpleFocusablePanel {
+
+ private DivElement focusElement;
+
+ public FocusElementPanel() {
+ focusElement = Document.get().createDivElement();
+ }
+
+ @Override
+ public void setWidget(Widget w) {
+ super.setWidget(w);
+ if (focusElement.getParentElement() == null) {
+ Style style = focusElement.getStyle();
+ style.setPosition(Position.FIXED);
+ style.setTop(0, Unit.PX);
+ style.setLeft(0, Unit.PX);
+ getElement().appendChild(focusElement);
+ /* Sink from focusElement too as focus and blur don't bubble */
+ DOM.sinkEvents(
+ (com.google.gwt.user.client.Element) focusElement.cast(),
+ Event.FOCUSEVENTS);
+ // revert to original, not focusable
+ getElement().setPropertyObject("tabIndex", null);
+ } else {
+ moveFocusElementAfterWidget();
+ }
+ }
+
+ /**
+ * Helper to keep focus element always in domChild[1]. Aids testing.
+ */
+ private void moveFocusElementAfterWidget() {
+ getElement().insertAfter(focusElement, getWidget().getElement());
+ }
+
+ @Override
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(
+ (Element) focusElement.cast());
+ } else {
+ FocusImpl.getFocusImplForPanel()
+ .blur((Element) focusElement.cast());
+ }
+ }
+
+ @Override
+ public void setTabIndex(int tabIndex) {
+ getElement().setTabIndex(-1);
+ if (focusElement != null) {
+ focusElement.setTabIndex(tabIndex);
+ }
+ }
+
+ /**
+ * @return the focus element
+ */
+ public Element getFocusElement() {
+ return focusElement.cast();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java
new file mode 100644
index 0000000000..2fac234587
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java
@@ -0,0 +1,110 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.terminal.gwt.client.Focusable;
+
+/**
+ * Adds keyboard focus to {@link FlexPanel}.
+ */
+public class FocusableFlexTable extends FlexTable implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable {
+
+ /**
+ * Default constructor.
+ */
+ public FocusableFlexTable() {
+ // make focusable, as we don't need access key magic we don't need to
+ // use FocusImpl.createFocusable
+ getElement().setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.
+ * google.gwt.event.dom.client.FocusHandler)
+ */
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google
+ * .gwt.event.dom.client.BlurHandler)
+ */
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler(
+ * com.google.gwt.event.dom.client.KeyDownHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler
+ * (com.google.gwt.event.dom.client.KeyPressHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ /**
+ * Sets the keyboard focus to the panel
+ *
+ * @param focus
+ * Should the panel have keyboard focus. If true the keyboard
+ * focus will be moved to the
+ */
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Focusable#focus()
+ */
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java
new file mode 100644
index 0000000000..258fe441af
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java
@@ -0,0 +1,105 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.terminal.gwt.client.Focusable;
+
+public class FocusableFlowPanel extends FlowPanel implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable {
+
+ /**
+ * Constructor
+ */
+ public FocusableFlowPanel() {
+ // make focusable, as we don't need access key magic we don't need to
+ // use FocusImpl.createFocusable
+ getElement().setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.
+ * google.gwt.event.dom.client.FocusHandler)
+ */
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google
+ * .gwt.event.dom.client.BlurHandler)
+ */
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler(
+ * com.google.gwt.event.dom.client.KeyDownHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler
+ * (com.google.gwt.event.dom.client.KeyPressHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ /**
+ * Sets/Removes the keyboard focus to the panel.
+ *
+ * @param focus
+ * If set to true then the focus is moved to the panel, if set to
+ * false the focus is removed
+ */
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ /**
+ * Focus the panel
+ */
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java
new file mode 100644
index 0000000000..0ba42eb861
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java
@@ -0,0 +1,184 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+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.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.HasScrollHandlers;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.ScrollPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+
+/**
+ * A scrollhandlers similar to {@link ScrollPanel}.
+ *
+ */
+public class FocusableScrollPanel extends SimpleFocusablePanel implements
+ HasScrollHandlers, ScrollHandler {
+
+ public FocusableScrollPanel() {
+ // Prevent IE standard mode bug when a AbsolutePanel is contained.
+ TouchScrollDelegate.enableTouchScrolling(this, getElement());
+ Style style = getElement().getStyle();
+ style.setProperty("zoom", "1");
+ style.setPosition(Position.RELATIVE);
+ }
+
+ private DivElement focusElement;
+
+ public FocusableScrollPanel(boolean useFakeFocusElement) {
+ this();
+ if (useFakeFocusElement) {
+ focusElement = Document.get().createDivElement();
+ }
+ }
+
+ private boolean useFakeFocusElement() {
+ return focusElement != null;
+ }
+
+ @Override
+ public void setWidget(Widget w) {
+ super.setWidget(w);
+ if (useFakeFocusElement()) {
+ if (focusElement.getParentElement() == null) {
+ Style style = focusElement.getStyle();
+ style.setPosition(Position.FIXED);
+ style.setTop(0, Unit.PX);
+ style.setLeft(0, Unit.PX);
+ getElement().appendChild(focusElement);
+ /* Sink from focusElemet too as focusa and blur don't bubble */
+ DOM.sinkEvents(
+ (com.google.gwt.user.client.Element) focusElement
+ .cast(), Event.FOCUSEVENTS);
+ // revert to original, not focusable
+ getElement().setPropertyObject("tabIndex", null);
+
+ } else {
+ moveFocusElementAfterWidget();
+ }
+ }
+ }
+
+ /**
+ * Helper to keep focus element always in domChild[1]. Aids testing.
+ */
+ private void moveFocusElementAfterWidget() {
+ getElement().insertAfter(focusElement, getWidget().getElement());
+ }
+
+ @Override
+ public void setFocus(boolean focus) {
+ if (useFakeFocusElement()) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(
+ (Element) focusElement.cast());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(
+ (Element) focusElement.cast());
+ }
+ } else {
+ super.setFocus(focus);
+ }
+ }
+
+ @Override
+ public void setTabIndex(int tabIndex) {
+ if (useFakeFocusElement()) {
+ getElement().setTabIndex(-1);
+ if (focusElement != null) {
+ focusElement.setTabIndex(tabIndex);
+ }
+ } else {
+ super.setTabIndex(tabIndex);
+ }
+ }
+
+ @Override
+ public HandlerRegistration addScrollHandler(ScrollHandler handler) {
+ return addDomHandler(handler, ScrollEvent.getType());
+ }
+
+ /**
+ * Gets the horizontal scroll position.
+ *
+ * @return the horizontal scroll position, in pixels
+ */
+ public int getHorizontalScrollPosition() {
+ return getElement().getScrollLeft();
+ }
+
+ /**
+ * Gets the vertical scroll position.
+ *
+ * @return the vertical scroll position, in pixels
+ */
+ public int getScrollPosition() {
+ if (getElement().getPropertyJSO("_vScrollTop") != null) {
+ return getElement().getPropertyInt("_vScrollTop");
+ } else {
+ return getElement().getScrollTop();
+ }
+ }
+
+ /**
+ * Sets the horizontal scroll position.
+ *
+ * @param position
+ * the new horizontal scroll position, in pixels
+ */
+ public void setHorizontalScrollPosition(int position) {
+ getElement().setScrollLeft(position);
+ }
+
+ /**
+ * Sets the vertical scroll position.
+ *
+ * @param position
+ * the new vertical scroll position, in pixels
+ */
+ public void setScrollPosition(int position) {
+ if (BrowserInfo.get().isAndroidWithBrokenScrollTop()
+ && BrowserInfo.get().requiresTouchScrollDelegate()) {
+ ArrayList<com.google.gwt.dom.client.Element> elements = TouchScrollDelegate
+ .getElements(getElement());
+ for (com.google.gwt.dom.client.Element el : elements) {
+ final Style style = el.getStyle();
+ style.setProperty("webkitTransform", "translate3d(0px,"
+ + -position + "px,0px)");
+ }
+ getElement().setPropertyInt("_vScrollTop", position);
+ } else {
+ getElement().setScrollTop(position);
+ }
+ }
+
+ @Override
+ public void onScroll(ScrollEvent event) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ focusElement.getStyle().setTop(getScrollPosition(), Unit.PX);
+ focusElement.getStyle().setLeft(getHorizontalScrollPosition(),
+ Unit.PX);
+ }
+ });
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Icon.java b/client/src/com/vaadin/terminal/gwt/client/ui/Icon.java
new file mode 100644
index 0000000000..b64605aac9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/Icon.java
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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 {
+ public static final String CLASSNAME = "v-icon";
+ private final ApplicationConnection client;
+ private String myUri;
+
+ public Icon(ApplicationConnection client) {
+ setElement(DOM.createImg());
+ DOM.setElementProperty(getElement(), "alt", "");
+ setStyleName(CLASSNAME);
+ this.client = client;
+ }
+
+ 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.translateVaadinUri(uidlUri);
+ DOM.setElementProperty(getElement(), "src", uri);
+ myUri = uidlUri;
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java
new file mode 100644
index 0000000000..380d96115c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptComponentConnector.java
@@ -0,0 +1,45 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.JavaScriptComponentState;
+import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper;
+import com.vaadin.terminal.gwt.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.ui.AbstractJavaScriptComponent;
+
+@Connect(AbstractJavaScriptComponent.class)
+public final class JavaScriptComponentConnector extends
+ AbstractComponentConnector implements HasJavaScriptConnectorHelper {
+
+ private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper(
+ this) {
+ @Override
+ protected void showInitProblem(
+ java.util.ArrayList<String> attemptedNames) {
+ getWidget().showNoInitFound(attemptedNames);
+ }
+ };
+
+ @Override
+ public JavaScriptWidget getWidget() {
+ return (JavaScriptWidget) super.getWidget();
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ helper.init();
+ }
+
+ @Override
+ public JavaScriptConnectorHelper getJavascriptConnectorHelper() {
+ return helper;
+ }
+
+ @Override
+ public JavaScriptComponentState getState() {
+ return (JavaScriptComponentState) super.getState();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java
new file mode 100644
index 0000000000..e6c3323893
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/JavaScriptWidget.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.ui.Widget;
+
+public class JavaScriptWidget extends Widget {
+ public JavaScriptWidget() {
+ setElement(Document.get().createDivElement());
+ }
+
+ public void showNoInitFound(ArrayList<String> attemptedNames) {
+ String message = "Could not initialize JavaScriptConnector because no JavaScript init function was found. Make sure one of these functions are defined: <ul>";
+ for (String name : attemptedNames) {
+ message += "<li>" + name + "</li>";
+ }
+ message += "</ul>";
+
+ getElement().setInnerHTML(message);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java
new file mode 100644
index 0000000000..9aafaa0bbf
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/LayoutClickEventHandler.java
@@ -0,0 +1,40 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.Element;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.LayoutClickRpc;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+
+public abstract class LayoutClickEventHandler extends AbstractClickEventHandler {
+
+ public static final String LAYOUT_CLICK_EVENT_IDENTIFIER = "lClick";
+
+ public LayoutClickEventHandler(ComponentConnector connector) {
+ this(connector, LAYOUT_CLICK_EVENT_IDENTIFIER);
+ }
+
+ public LayoutClickEventHandler(ComponentConnector connector,
+ String clickEventIdentifier) {
+ super(connector, clickEventIdentifier);
+ }
+
+ protected abstract ComponentConnector getChildComponent(Element element);
+
+ protected ComponentConnector getChildComponent(NativeEvent event) {
+ return getChildComponent((Element) event.getEventTarget().cast());
+ }
+
+ @Override
+ protected void fireClick(NativeEvent event) {
+ MouseEventDetails mouseDetails = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event, getRelativeToElement());
+ getLayoutClickRPC().layoutClick(mouseDetails, getChildComponent(event));
+ }
+
+ protected abstract LayoutClickRpc getLayoutClickRPC();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java
new file mode 100644
index 0000000000..6d0271ee40
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ManagedLayout.java
@@ -0,0 +1,10 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+
+public interface ManagedLayout extends ComponentConnector {
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java
new file mode 100644
index 0000000000..ccf90b2285
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.shared.communication.URLReference;
+import com.vaadin.shared.ui.AbstractMediaState;
+import com.vaadin.shared.ui.MediaControl;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+
+public abstract class MediaBaseConnector extends AbstractComponentConnector {
+
+ @Override
+ protected void init() {
+ super.init();
+
+ registerRpc(MediaControl.class, new MediaControl() {
+ @Override
+ public void play() {
+ getWidget().play();
+ }
+
+ @Override
+ public void pause() {
+ getWidget().pause();
+ }
+ });
+ }
+
+ @Override
+ public AbstractMediaState getState() {
+ return (AbstractMediaState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ getWidget().setControls(getState().isShowControls());
+ getWidget().setAutoplay(getState().isAutoplay());
+ getWidget().setMuted(getState().isMuted());
+ for (int i = 0; i < getState().getSources().size(); i++) {
+ URLReference source = getState().getSources().get(i);
+ String sourceType = getState().getSourceTypes().get(i);
+ getWidget().addSource(source.getURL(), sourceType);
+ }
+ setAltText(getState().getAltText());
+ }
+
+ @Override
+ public VMediaBase getWidget() {
+ return (VMediaBase) super.getWidget();
+ }
+
+ private void setAltText(String altText) {
+
+ if (altText == null || "".equals(altText)) {
+ altText = getDefaultAltHtml();
+ } else if (!getState().isHtmlContentAllowed()) {
+ altText = Util.escapeHTML(altText);
+ }
+ getWidget().setAltText(altText);
+ }
+
+ /**
+ * @return the default HTML to show users with browsers that do not support
+ * HTML5 media markup.
+ */
+ protected abstract String getDefaultAltHtml();
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java b/client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java
new file mode 100644
index 0000000000..feb7494f87
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/PostLayoutListener.java
@@ -0,0 +1,8 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+public interface PostLayoutListener {
+ public void postLayout();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java
new file mode 100644
index 0000000000..40454345bc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java
@@ -0,0 +1,298 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.google.gwt.core.client.Scheduler;
+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.ui.HasWidgets;
+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.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.richtextarea.VRichTextArea;
+
+/**
+ * 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 Vaadin Ltd
+ */
+public class ShortcutActionHandler {
+
+ /**
+ * An interface implemented by those users of this helper class that want to
+ * support special components like {@link VRichTextArea} that don't properly
+ * propagate key down events. Those components can build support for
+ * shortcut actions by traversing the closest
+ * {@link ShortcutActionHandlerOwner} from the component hierarchy an
+ * passing keydown events to {@link ShortcutActionHandler}.
+ */
+ public interface ShortcutActionHandlerOwner extends HasWidgets {
+
+ /**
+ * Returns the ShortCutActionHandler currently used or null if there is
+ * currently no shortcutactionhandler
+ */
+ ShortcutActionHandler getShortcutActionHandler();
+ }
+
+ /**
+ * A focusable {@link ComponentConnector} implementing this interface will
+ * be notified before shortcut actions are handled if it will be the target
+ * of the action (most commonly means it is the focused component during the
+ * keyboard combination is triggered by the user).
+ */
+ public interface BeforeShortcutActionListener extends ComponentConnector {
+ /**
+ * This method is called by ShortcutActionHandler before firing the
+ * shortcut if the Paintable is currently focused (aka the target of the
+ * shortcut action). Eg. a field can update its possibly changed value
+ * to the server before shortcut action is fired.
+ *
+ * @param e
+ * the event that triggered the shortcut action
+ */
+ public void onBeforeShortcutAction(Event e);
+ }
+
+ private final ArrayList<ShortcutAction> actions = new ArrayList<ShortcutAction>();
+ 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, ComponentConnector target) {
+ final int modifiers = KeyboardListenerCollection
+ .getKeyboardModifiers(event);
+ final char keyCode = (char) DOM.eventGetKeyCode(event);
+ final ShortcutKeyCombination kc = new ShortcutKeyCombination(keyCode,
+ modifiers);
+ final Iterator<ShortcutAction> it = actions.iterator();
+ while (it.hasNext()) {
+ final ShortcutAction a = it.next();
+ if (a.getShortcutCombination().equals(kc)) {
+ fireAction(event, a, target);
+ break;
+ }
+ }
+
+ }
+
+ public void handleKeyboardEvent(final Event event) {
+ handleKeyboardEvent(event, null);
+ }
+
+ private void fireAction(final Event event, final ShortcutAction a,
+ ComponentConnector target) {
+ final Element et = DOM.eventGetTarget(event);
+ if (target == null) {
+ target = Util.findPaintable(client, et);
+ }
+ final ComponentConnector finalTarget = target;
+
+ event.preventDefault();
+
+ /*
+ * The target component might have unpublished changes, try to
+ * synchronize them before firing shortcut action.
+ */
+ if (finalTarget instanceof BeforeShortcutActionListener) {
+ ((BeforeShortcutActionListener) finalTarget)
+ .onBeforeShortcutAction(event);
+ } else {
+ shakeTarget(et);
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ shakeTarget(et);
+ }
+ });
+ }
+
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ if (finalTarget != null) {
+ client.updateVariable(paintableId, "actiontarget",
+ finalTarget, false);
+ }
+ client.updateVariable(paintableId, "action", a.getKey(), true);
+ }
+ });
+ }
+
+ /**
+ * We try to fire value change in the component the key combination was
+ * typed. Eg. textfield may contain newly typed text that is expected to be
+ * sent to server. This is done by removing focus and then returning it
+ * immediately back to target element.
+ * <p>
+ * This is practically a hack and should be replaced with an interface
+ * {@link BeforeShortcutActionListener} via widgets could be notified when
+ * they should fire value change. Big task for TextFields, DateFields and
+ * various selects.
+ *
+ * <p>
+ * TODO separate opera impl with generator
+ */
+ private static void shakeTarget(final Element e) {
+ blur(e);
+ if (BrowserInfo.get().isOpera()) {
+ // will mess up with focus and blur event if the focus is not
+ // deferred. Will cause a small flickering, so not doing it for all
+ // browsers.
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ focus(e);
+ }
+ });
+ } else {
+ focus(e);
+ }
+ }
+
+ private static native void blur(Element e)
+ /*-{
+ if(e.blur) {
+ e.blur();
+ }
+ }-*/;
+
+ private static native void focus(Element e)
+ /*-{
+ if(e.blur) {
+ e.focus();
+ }
+ }-*/;
+
+}
+
+class ShortcutKeyCombination {
+
+ public static final int SHIFT = 16;
+ public static final int CTRL = 17;
+ public static final int ALT = 18;
+ public static final int META = 91;
+
+ 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;
+
+ 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;
+ case META:
+ modifiersMask = modifiersMask
+ | KeyboardListener.MODIFIER_META;
+ 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/client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java
new file mode 100644
index 0000000000..ec62b82ce1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java
@@ -0,0 +1,79 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.terminal.gwt.client.Focusable;
+
+/**
+ * Compared to FocusPanel in GWT this panel does not support eg. accesskeys, but
+ * is simpler by its dom hierarchy nor supports focusing via java api.
+ */
+public class SimpleFocusablePanel extends SimplePanel implements
+ HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers,
+ HasKeyPressHandlers, Focusable {
+
+ public SimpleFocusablePanel() {
+ // make focusable, as we don't need access key magic we don't need to
+ // use FocusImpl.createFocusable
+ setTabIndex(0);
+ }
+
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
+ return addDomHandler(handler, KeyUpEvent.getType());
+ }
+
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+
+ public void setTabIndex(int tabIndex) {
+ getElement().setTabIndex(tabIndex);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java
new file mode 100644
index 0000000000..9ccb29a750
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/SimpleManagedLayout.java
@@ -0,0 +1,8 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+public interface SimpleManagedLayout extends ManagedLayout {
+ public void layout();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java b/client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java
new file mode 100644
index 0000000000..e7fcf8d424
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java
@@ -0,0 +1,50 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ComponentLocator;
+
+/**
+ * Interface implemented by {@link Widget}s which can provide identifiers for at
+ * least one element inside the component. Used by {@link ComponentLocator}.
+ *
+ */
+public interface SubPartAware {
+
+ /**
+ * Locates an element inside a component using the identifier provided in
+ * {@code subPart}. The {@code subPart} identifier is component specific and
+ * may be any string of characters, numbers, space characters and brackets.
+ *
+ * @param subPart
+ * The identifier for the element inside the component
+ * @return The element identified by subPart or null if the element could
+ * not be found.
+ */
+ Element getSubPartElement(String subPart);
+
+ /**
+ * Provides an identifier that identifies the element within the component.
+ * The {@code subElement} is a part of the component and must never be null.
+ * <p>
+ * <b>Note!</b>
+ * {@code getSubPartElement(getSubPartName(element)) == element} is <i>not
+ * always</i> true. A component can choose to provide a more generic
+ * identifier for any given element if the results of all interactions with
+ * {@code subElement} are the same as interactions with the element
+ * identified by the return value. For example a button can return an
+ * identifier for the root element even though a DIV inside the button was
+ * passed as {@code subElement} because interactions with the DIV and the
+ * root button element produce the same result.
+ *
+ * @param subElement
+ * The element the identifier string should uniquely identify
+ * @return An identifier that uniquely identifies {@code subElement} or null
+ * if no identifier could be provided.
+ */
+ String getSubPartName(Element subElement);
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java b/client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java
new file mode 100644
index 0000000000..16c32acecc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/TouchScrollDelegate.java
@@ -0,0 +1,657 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Touch;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.VConsole;
+
+/**
+ * Provides one finger touch scrolling for elements with once scrollable
+ * elements inside. One widget can have several of these scrollable elements.
+ * Scrollable elements are provided in the constructor. Users must pass
+ * touchStart events to this delegate, from there on the delegate takes over
+ * with an event preview. Other touch events needs to be sunken though.
+ * <p>
+ * This is bit similar as Scroller class in GWT expenses example, but ideas
+ * drawn from iscroll.js project:
+ * <ul>
+ * <li>uses GWT event mechanism.
+ * <li>uses modern CSS trick during scrolling for smoother experience:
+ * translate3d and transitions
+ * </ul>
+ * <p>
+ * Scroll event should only happen when the "touch scrolling actually ends".
+ * Later we might also tune this so that a scroll event happens if user stalls
+ * her finger long enought.
+ *
+ * TODO static getter for active touch scroll delegate. Components might need to
+ * prevent scrolling in some cases. Consider Table with drag and drop, or drag
+ * and drop in scrollable area. Optimal implementation might be to start the
+ * drag and drop only if user keeps finger down for a moment, otherwise do the
+ * scroll. In this case, the draggable component would need to cancel scrolling
+ * in a timer after touchstart event and take over from there.
+ *
+ * TODO support scrolling horizontally
+ *
+ * TODO cancel if user add second finger to the screen (user expects a gesture).
+ *
+ * TODO "scrollbars", see e.g. iscroll.js
+ *
+ * TODO write an email to sjobs ät apple dot com and beg for this feature to be
+ * built into webkit. Seriously, we should try to lobbying this to webkit folks.
+ * This sure ain't our business to implement this with javascript.
+ *
+ * TODO collect all general touch related constant to better place.
+ *
+ * @author Matti Tahvonen, Vaadin Ltd
+ */
+public class TouchScrollDelegate implements NativePreviewHandler {
+
+ private static final double FRICTION = 0.002;
+ private static final double DECELERATION = 0.002;
+ private static final int MAX_DURATION = 1500;
+ private int origY;
+ private HashSet<Element> scrollableElements;
+ private Element scrolledElement;
+ private int origScrollTop;
+ private HandlerRegistration handlerRegistration;
+ private double lastAnimatedTranslateY;
+ private int lastClientY;
+ private int deltaScrollPos;
+ private boolean transitionOn = false;
+ private int finalScrollTop;
+ private ArrayList<Element> layers;
+ private boolean moved;
+ private ScrollHandler scrollHandler;
+
+ private static TouchScrollDelegate activeScrollDelegate;
+
+ private static final boolean androidWithBrokenScrollTop = BrowserInfo.get()
+ .isAndroidWithBrokenScrollTop();
+
+ /**
+ * A helper class for making a widget scrollable. Uses native scrolling if
+ * supported by the browser, otherwise registers a touch start handler
+ * delegating to a TouchScrollDelegate instance.
+ */
+ public static class TouchScrollHandler implements TouchStartHandler {
+
+ private static final String SCROLLABLE_CLASSNAME = "v-scrollable";
+
+ private final TouchScrollDelegate delegate;
+ private final boolean requiresDelegate = BrowserInfo.get()
+ .requiresTouchScrollDelegate();
+
+ /**
+ * Constructs a scroll handler for the given widget.
+ *
+ * @param widget
+ * The widget that contains scrollable elements
+ * @param scrollables
+ * The elements of the widget that should be scrollable.
+ */
+ public TouchScrollHandler(Widget widget, Element... scrollables) {
+ if (requiresDelegate) {
+ delegate = new TouchScrollDelegate();
+ widget.addDomHandler(this, TouchStartEvent.getType());
+ } else {
+ delegate = null;
+ }
+ setElements(scrollables);
+ }
+
+ @Override
+ public void onTouchStart(TouchStartEvent event) {
+ assert delegate != null;
+ delegate.onTouchStart(event);
+ }
+
+ public void debug(Element e) {
+ VConsole.log("Classes: " + e.getClassName() + " overflow: "
+ + e.getStyle().getProperty("overflow") + " w-o-s: "
+ + e.getStyle().getProperty("WebkitOverflowScrolling"));
+ }
+
+ /**
+ * Registers the given element as scrollable.
+ */
+ public void addElement(Element scrollable) {
+ scrollable.addClassName(SCROLLABLE_CLASSNAME);
+ if (requiresDelegate) {
+ delegate.scrollableElements.add(scrollable);
+ }
+ }
+
+ /**
+ * Unregisters the given element as scrollable. Should be called when a
+ * previously-registered element is removed from the DOM to prevent
+ * memory leaks.
+ */
+ public void removeElement(Element scrollable) {
+ scrollable.removeClassName(SCROLLABLE_CLASSNAME);
+ if (requiresDelegate) {
+ delegate.scrollableElements.remove(scrollable);
+ }
+ }
+
+ /**
+ * Registers the given elements as scrollable, removing previously
+ * registered scrollables from this handler.
+ *
+ * @param scrollables
+ * The elements that should be scrollable
+ */
+ public void setElements(Element... scrollables) {
+ if (requiresDelegate) {
+ for (Element e : delegate.scrollableElements) {
+ e.removeClassName(SCROLLABLE_CLASSNAME);
+ }
+ delegate.scrollableElements.clear();
+ }
+ for (Element e : scrollables) {
+ addElement(e);
+ }
+ }
+ }
+
+ /**
+ * Makes the given elements scrollable, either natively or by using a
+ * TouchScrollDelegate, depending on platform capabilities.
+ *
+ * @param widget
+ * The widget that contains scrollable elements
+ * @param scrollables
+ * The elements inside the widget that should be scrollable
+ * @return A scroll handler for the given widget.
+ */
+ public static TouchScrollHandler enableTouchScrolling(Widget widget,
+ Element... scrollables) {
+ return new TouchScrollHandler(widget, scrollables);
+ }
+
+ public TouchScrollDelegate(Element... elements) {
+ setElements(elements);
+ }
+
+ public void setScrollHandler(ScrollHandler scrollHandler) {
+ this.scrollHandler = scrollHandler;
+ }
+
+ public static TouchScrollDelegate getActiveScrollDelegate() {
+ return activeScrollDelegate;
+ }
+
+ /**
+ * Has user moved the touch.
+ *
+ * @return
+ */
+ public boolean isMoved() {
+ return moved;
+ }
+
+ /**
+ * Forces the scroll delegate to cancels scrolling process. Can be called by
+ * users if they e.g. decide to handle touch event by themselves after all
+ * (e.g. a pause after touch start before moving touch -> interpreted as
+ * long touch/click or drag start).
+ */
+ public void stopScrolling() {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ if (moved) {
+ moveTransformationToScrolloffset();
+ } else {
+ activeScrollDelegate = null;
+ }
+ }
+
+ public void onTouchStart(TouchStartEvent event) {
+ if (activeScrollDelegate == null && event.getTouches().length() == 1) {
+ NativeEvent nativeEvent = event.getNativeEvent();
+ doTouchStart(nativeEvent);
+ } else {
+ /*
+ * Touch scroll is currenly on (possibly bouncing). Ignore.
+ */
+ }
+ }
+
+ private void doTouchStart(NativeEvent nativeEvent) {
+ if (transitionOn) {
+ momentum.cancel();
+ }
+ Touch touch = nativeEvent.getTouches().get(0);
+ if (detectScrolledElement(touch)) {
+ VConsole.log("TouchDelegate takes over");
+ nativeEvent.stopPropagation();
+ handlerRegistration = Event.addNativePreviewHandler(this);
+ activeScrollDelegate = this;
+ origY = touch.getClientY();
+ yPositions[0] = origY;
+ eventTimeStamps[0] = getTimeStamp();
+ nextEvent = 1;
+
+ origScrollTop = getScrollTop();
+ VConsole.log("ST" + origScrollTop);
+
+ moved = false;
+ // event.preventDefault();
+ // event.stopPropagation();
+ }
+ }
+
+ private int getScrollTop() {
+ if (androidWithBrokenScrollTop) {
+ if (scrolledElement.getPropertyJSO("_vScrollTop") != null) {
+ return scrolledElement.getPropertyInt("_vScrollTop");
+ }
+ return 0;
+ }
+ return scrolledElement.getScrollTop();
+ }
+
+ private void onTransitionEnd() {
+ if (finalScrollTop < 0) {
+ animateToScrollPosition(0, finalScrollTop);
+ finalScrollTop = 0;
+ } else if (finalScrollTop > getMaxFinalY()) {
+ animateToScrollPosition(getMaxFinalY(), finalScrollTop);
+ finalScrollTop = getMaxFinalY();
+ } else {
+ moveTransformationToScrolloffset();
+ }
+ }
+
+ private void animateToScrollPosition(int to, int from) {
+ int dist = Math.abs(to - from);
+ int time = getAnimationTimeForDistance(dist);
+ if (time <= 0) {
+ time = 1; // get animation and transition end event
+ }
+ VConsole.log("Animate " + time + " " + from + " " + to);
+ int translateTo = -to + origScrollTop;
+ int fromY = -from + origScrollTop;
+ if (androidWithBrokenScrollTop) {
+ fromY -= origScrollTop;
+ translateTo -= origScrollTop;
+ }
+ translateTo(time, fromY, translateTo);
+ }
+
+ private int getAnimationTimeForDistance(int dist) {
+ return 350; // 350ms seems to work quite fine for all distances
+ // if (dist < 0) {
+ // dist = -dist;
+ // }
+ // return MAX_DURATION * dist / (scrolledElement.getClientHeight() * 3);
+ }
+
+ /**
+ * Called at the end of scrolling. Moves possible translate values to
+ * scrolltop, causing onscroll event.
+ */
+ private void moveTransformationToScrolloffset() {
+ if (androidWithBrokenScrollTop) {
+ scrolledElement.setPropertyInt("_vScrollTop", finalScrollTop);
+ if (scrollHandler != null) {
+ scrollHandler.onScroll(null);
+ }
+ } else {
+ for (Element el : layers) {
+ Style style = el.getStyle();
+ style.setProperty("webkitTransform", "translate3d(0,0,0)");
+ }
+ scrolledElement.setScrollTop(finalScrollTop);
+ }
+ activeScrollDelegate = null;
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+
+ /**
+ * Detects if a touch happens on a predefined element and the element has
+ * something to scroll.
+ *
+ * @param touch
+ * @return
+ */
+ private boolean detectScrolledElement(Touch touch) {
+ Element target = touch.getTarget().cast();
+ for (Element el : scrollableElements) {
+ if (el.isOrHasChild(target)
+ && el.getScrollHeight() > el.getClientHeight()) {
+ scrolledElement = el;
+ layers = getElements(scrolledElement);
+ return true;
+
+ }
+ }
+ return false;
+ }
+
+ public static ArrayList<Element> getElements(Element scrolledElement2) {
+ NodeList<Node> childNodes = scrolledElement2.getChildNodes();
+ ArrayList<Element> l = new ArrayList<Element>();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node item = childNodes.getItem(i);
+ if (item.getNodeType() == Node.ELEMENT_NODE) {
+ l.add((Element) item);
+ }
+ }
+ return l;
+ }
+
+ private void onTouchMove(NativeEvent event) {
+ if (!moved) {
+ double l = (getTimeStamp() - eventTimeStamps[0]);
+ VConsole.log(l + " ms from start to move");
+ }
+ boolean handleMove = readPositionAndSpeed(event);
+ if (handleMove) {
+ int deltaScrollTop = origY - lastClientY;
+ int finalPos = origScrollTop + deltaScrollTop;
+ if (finalPos > getMaxFinalY()) {
+ // spring effect at the end
+ int overscroll = (deltaScrollTop + origScrollTop)
+ - getMaxFinalY();
+ overscroll = overscroll / 2;
+ if (overscroll > getMaxOverScroll()) {
+ overscroll = getMaxOverScroll();
+ }
+ deltaScrollTop = getMaxFinalY() + overscroll - origScrollTop;
+ } else if (finalPos < 0) {
+ // spring effect at the beginning
+ int overscroll = finalPos / 2;
+ if (-overscroll > getMaxOverScroll()) {
+ overscroll = -getMaxOverScroll();
+ }
+ deltaScrollTop = overscroll - origScrollTop;
+ }
+ quickSetScrollPosition(0, deltaScrollTop);
+ moved = true;
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ private void quickSetScrollPosition(int deltaX, int deltaY) {
+ deltaScrollPos = deltaY;
+ if (androidWithBrokenScrollTop) {
+ deltaY += origScrollTop;
+ translateTo(-deltaY);
+ } else {
+ translateTo(-deltaScrollPos);
+ }
+ }
+
+ private static final int EVENTS_FOR_SPEED_CALC = 3;
+ public static final int SIGNIFICANT_MOVE_THRESHOLD = 3;
+ private int[] yPositions = new int[EVENTS_FOR_SPEED_CALC];
+ private double[] eventTimeStamps = new double[EVENTS_FOR_SPEED_CALC];
+ private int nextEvent = 0;
+ private Animation momentum;
+
+ /**
+ *
+ * @param event
+ * @return
+ */
+ private boolean readPositionAndSpeed(NativeEvent event) {
+ Touch touch = event.getChangedTouches().get(0);
+ lastClientY = touch.getClientY();
+ int eventIndx = nextEvent++;
+ eventIndx = eventIndx % EVENTS_FOR_SPEED_CALC;
+ eventTimeStamps[eventIndx] = getTimeStamp();
+ yPositions[eventIndx] = lastClientY;
+ return isMovedSignificantly();
+ }
+
+ private boolean isMovedSignificantly() {
+ return moved ? moved
+ : Math.abs(origY - lastClientY) >= SIGNIFICANT_MOVE_THRESHOLD;
+ }
+
+ private void onTouchEnd(NativeEvent event) {
+ if (!moved) {
+ activeScrollDelegate = null;
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ return;
+ }
+
+ int currentY = origScrollTop + deltaScrollPos;
+
+ int maxFinalY = getMaxFinalY();
+
+ int pixelsToMove;
+ int finalY;
+ int duration = -1;
+ if (currentY > maxFinalY) {
+ // we are over the max final pos, animate to end
+ pixelsToMove = maxFinalY - currentY;
+ finalY = maxFinalY;
+ } else if (currentY < 0) {
+ // we are below the max final pos, animate to beginning
+ pixelsToMove = -currentY;
+ finalY = 0;
+ } else {
+ double pixelsPerMs = calculateSpeed();
+ // we are currently within scrollable area, calculate pixels that
+ // we'll move due to momentum
+ VConsole.log("pxPerMs" + pixelsPerMs);
+ pixelsToMove = (int) (0.5 * pixelsPerMs * pixelsPerMs / FRICTION);
+ if (pixelsPerMs < 0) {
+ pixelsToMove = -pixelsToMove;
+ }
+ // VConsole.log("pixels to move" + pixelsToMove);
+
+ finalY = currentY + pixelsToMove;
+
+ if (finalY > maxFinalY + getMaxOverScroll()) {
+ // VConsole.log("To max overscroll");
+ finalY = getMaxFinalY() + getMaxOverScroll();
+ int fixedPixelsToMove = finalY - currentY;
+ pixelsToMove = fixedPixelsToMove;
+ } else if (finalY < 0 - getMaxOverScroll()) {
+ // VConsole.log("to min overscroll");
+ finalY = -getMaxOverScroll();
+ int fixedPixelsToMove = finalY - currentY;
+ pixelsToMove = fixedPixelsToMove;
+ } else {
+ duration = (int) (Math.abs(pixelsPerMs / DECELERATION));
+ }
+ }
+ if (duration == -1) {
+ // did not keep in side borders or was outside borders, calculate
+ // a good enough duration based on pixelsToBeMoved.
+ duration = getAnimationTimeForDistance(pixelsToMove);
+ }
+ if (duration > MAX_DURATION) {
+ VConsole.log("Max animation time. " + duration);
+ duration = MAX_DURATION;
+ }
+ finalScrollTop = finalY;
+
+ if (Math.abs(pixelsToMove) < 3 || duration < 20) {
+ VConsole.log("Small 'momentum' " + pixelsToMove + " | " + duration
+ + " Skipping animation,");
+ moveTransformationToScrolloffset();
+ return;
+ }
+
+ int translateTo = -finalY + origScrollTop;
+ int fromY = -currentY + origScrollTop;
+ if (androidWithBrokenScrollTop) {
+ fromY -= origScrollTop;
+ translateTo -= origScrollTop;
+ }
+ translateTo(duration, fromY, translateTo);
+ }
+
+ private double calculateSpeed() {
+ if (nextEvent < EVENTS_FOR_SPEED_CALC) {
+ VConsole.log("Not enough data for speed calculation");
+ // not enough data for decent speed calculation, no momentum :-(
+ return 0;
+ }
+ int idx = nextEvent % EVENTS_FOR_SPEED_CALC;
+ final int firstPos = yPositions[idx];
+ final double firstTs = eventTimeStamps[idx];
+ idx += EVENTS_FOR_SPEED_CALC;
+ idx--;
+ idx = idx % EVENTS_FOR_SPEED_CALC;
+ final int lastPos = yPositions[idx];
+ final double lastTs = eventTimeStamps[idx];
+ // speed as in change of scrolltop == -speedOfTouchPos
+ return (firstPos - lastPos) / (lastTs - firstTs);
+
+ }
+
+ /**
+ * Note positive scrolltop moves layer up, positive translate moves layer
+ * down.
+ */
+ private void translateTo(double translateY) {
+ for (Element el : layers) {
+ Style style = el.getStyle();
+ style.setProperty("webkitTransform", "translate3d(0px,"
+ + translateY + "px,0px)");
+ }
+ }
+
+ /**
+ * Note positive scrolltop moves layer up, positive translate moves layer
+ * down.
+ *
+ * @param duration
+ */
+ private void translateTo(int duration, final int fromY, final int finalY) {
+ if (duration > 0) {
+ transitionOn = true;
+
+ momentum = new Animation() {
+
+ @Override
+ protected void onUpdate(double progress) {
+ lastAnimatedTranslateY = (fromY + (finalY - fromY)
+ * progress);
+ translateTo(lastAnimatedTranslateY);
+ }
+
+ @Override
+ protected double interpolate(double progress) {
+ return 1 + Math.pow(progress - 1, 3);
+ }
+
+ @Override
+ protected void onComplete() {
+ super.onComplete();
+ transitionOn = false;
+ onTransitionEnd();
+ }
+
+ @Override
+ protected void onCancel() {
+ int delta = (int) (finalY - lastAnimatedTranslateY);
+ finalScrollTop -= delta;
+ moveTransformationToScrolloffset();
+ transitionOn = false;
+ }
+ };
+ momentum.run(duration);
+ }
+ }
+
+ private int getMaxOverScroll() {
+ return androidWithBrokenScrollTop ? 0 : scrolledElement
+ .getClientHeight() / 3;
+ }
+
+ private int getMaxFinalY() {
+ return scrolledElement.getScrollHeight()
+ - scrolledElement.getClientHeight();
+ }
+
+ @Override
+ public void onPreviewNativeEvent(NativePreviewEvent event) {
+ int typeInt = event.getTypeInt();
+ if (transitionOn) {
+ /*
+ * TODO allow starting new events. See issue in onTouchStart
+ */
+ event.cancel();
+
+ if (typeInt == Event.ONTOUCHSTART) {
+ doTouchStart(event.getNativeEvent());
+ }
+ return;
+ }
+ switch (typeInt) {
+ case Event.ONTOUCHMOVE:
+ if (!event.isCanceled()) {
+ onTouchMove(event.getNativeEvent());
+ if (moved) {
+ event.cancel();
+ }
+ }
+ break;
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ if (!event.isCanceled()) {
+ if (moved) {
+ event.cancel();
+ }
+ onTouchEnd(event.getNativeEvent());
+ }
+ break;
+ case Event.ONMOUSEMOVE:
+ if (moved) {
+ // no debug message, mobile safari generates these for some
+ // compatibility purposes.
+ event.cancel();
+ }
+ break;
+ default:
+ VConsole.log("Non touch event:" + event.getNativeEvent().getType());
+ event.cancel();
+ break;
+ }
+ }
+
+ public void setElements(Element[] elements) {
+ scrollableElements = new HashSet<Element>(Arrays.asList(elements));
+ }
+
+ /**
+ * long calcucation are not very efficient in GWT, so this helper method
+ * returns timestamp in double.
+ *
+ * @return
+ */
+ public static double getTimeStamp() {
+ return Duration.currentTimeMillis();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java b/client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java
new file mode 100644
index 0000000000..9efe369644
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+/**
+ * This class is used for "row actions" in VTree 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" Vaadin 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/client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java b/client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java
new file mode 100644
index 0000000000..221a409511
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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/vaadin/terminal/gwt/public/default/tree/img/expanded
+ * .png
+ */
+ @Override
+ AbstractImagePrototype treeOpen();
+
+ /**
+ * An image indicating a closed branch.
+ *
+ * @return a prototype of this image
+ * @gwt.resource com/vaadin/terminal/gwt/public/default/tree/img/collapsed
+ * .png
+ */
+ @Override
+ AbstractImagePrototype treeClosed();
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java
new file mode 100644
index 0000000000..83ac97458e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/UnknownComponentConnector.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+public class UnknownComponentConnector extends AbstractComponentConnector {
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public VUnknownComponent getWidget() {
+ return (VUnknownComponent) super.getWidget();
+ }
+
+ public void setServerSideClassName(String serverClassName) {
+ getWidget()
+ .setCaption(
+ "Widgetset does not contain implementation for "
+ + serverClassName
+ + ". Check its component connector's @Connect mapping, widgetsets "
+ + "GWT module description file and re-compile your"
+ + " widgetset. In case you have downloaded a vaadin"
+ + " add-on package, you might want to refer to "
+ + "<a href='http://vaadin.com/using-addons'>add-on "
+ + "instructions</a>.");
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java b/client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java
new file mode 100644
index 0000000000..607abe893a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/VContextMenu.java
@@ -0,0 +1,280 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+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.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Command;
+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.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class VContextMenu extends VOverlay implements SubPartAware {
+
+ private ActionOwner actionOwner;
+
+ private final CMenuBar menu = new CMenuBar();
+
+ private int left;
+
+ private int top;
+
+ private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(100,
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ imagesLoaded();
+ }
+ });
+
+ /**
+ * 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 VContextMenu() {
+ super(true, false, true);
+ setWidget(menu);
+ setStyleName("v-contextmenu");
+ }
+
+ protected void imagesLoaded() {
+ if (isVisible()) {
+ show();
+ }
+ }
+
+ /**
+ * Sets the element from which to build menu
+ *
+ * @param ao
+ */
+ public void setActionOwner(ActionOwner ao) {
+ actionOwner = ao;
+ }
+
+ /**
+ * Shows context menu at given location IF it contain at least one item.
+ *
+ * @param left
+ * @param top
+ */
+ public void showAt(int left, int top) {
+ final Action[] actions = actionOwner.getActions();
+ if (actions == null || actions.length == 0) {
+ // Only show if there really are actions
+ return;
+ }
+ this.left = left;
+ this.top = top;
+ menu.clearItems();
+ for (int i = 0; i < actions.length; i++) {
+ final Action a = actions[i];
+ menu.addItem(new MenuItem(a.getHTML(), true, a));
+ }
+
+ // Attach onload listeners to all images
+ Util.sinkOnloadForImages(menu.getElement());
+
+ setPopupPositionAndShow(new PositionCallback() {
+ @Override
+ 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 = VContextMenu.this.left;
+ int top = VContextMenu.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);
+
+ /*
+ * Move keyboard focus to menu, deferring the focus setting so
+ * the focus is certainly moved to the menu in all browser after
+ * the positioning has been done.
+ */
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ // Focus the menu.
+ menu.setFocus(true);
+
+ // Unselect previously selected items
+ menu.selectItem(null);
+ }
+ });
+
+ }
+ });
+ }
+
+ 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 implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers,
+ Focusable, LoadHandler {
+ public CMenuBar() {
+ super(true);
+ addDomHandler(this, LoadEvent.getType());
+ }
+
+ @Override
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ super.onPopupClosed(sender, autoClosed);
+
+ // make focusable, as we don't need access key magic we don't need
+ // to
+ // use FocusImpl.createFocusable
+ getElement().setTabIndex(0);
+
+ 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); }
+ */
+
+ private MenuItem getItem(int index) {
+ return super.getItems().get(index);
+ }
+
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+
+ @Override
+ public void onLoad(LoadEvent event) {
+ // Handle icon onload events to ensure shadow is resized correctly
+ delayedImageLoadExecutioner.trigger();
+ }
+
+ }
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ int index = Integer.parseInt(subPart.substring(6));
+ // ApplicationConnection.getConsole().log(
+ // "Searching element for selection index " + index);
+ MenuItem item = menu.getItem(index);
+ // ApplicationConnection.getConsole().log("Item: " + item);
+ // Item refers to the td, which is the parent of the clickable element
+ return item.getElement().getFirstChildElement().cast();
+ }
+
+ @Override
+ 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;
+ }
+ }
+
+ /**
+ * Hides context menu if it is currently shown by given action owner.
+ *
+ * @param actionOwner
+ */
+ public void ensureHidden(ActionOwner actionOwner) {
+ if (this.actionOwner == actionOwner) {
+ hide();
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java b/client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java
new file mode 100644
index 0000000000..aac8ca5ee7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/VLazyExecutor.java
@@ -0,0 +1,52 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.user.client.Timer;
+
+/**
+ * Executes the given command {@code delayMs} milliseconds after a call to
+ * {@link #trigger()}. Calling {@link #trigger()} again before the command has
+ * been executed causes the execution to be rescheduled to {@code delayMs} after
+ * the second call.
+ *
+ */
+public class VLazyExecutor {
+
+ private Timer timer;
+ private int delayMs;
+ private ScheduledCommand cmd;
+
+ /**
+ * @param delayMs
+ * Delay in milliseconds to wait before executing the command
+ * @param cmd
+ * The command to execute
+ */
+ public VLazyExecutor(int delayMs, ScheduledCommand cmd) {
+ this.delayMs = delayMs;
+ this.cmd = cmd;
+ }
+
+ /**
+ * Triggers execution of the command. Each call reschedules any existing
+ * execution to {@link #delayMs} milliseconds from that point in time.
+ */
+ public void trigger() {
+ if (timer == null) {
+ timer = new Timer() {
+ @Override
+ public void run() {
+ timer = null;
+ cmd.execute();
+ }
+ };
+ }
+ // Schedule automatically cancels any old schedule
+ timer.schedule(delayMs);
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java b/client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java
new file mode 100644
index 0000000000..40696ccec5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/VMediaBase.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.MediaElement;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+
+public abstract class VMediaBase extends Widget {
+
+ private MediaElement media;
+
+ /**
+ * Sets the MediaElement that is to receive all commands and properties.
+ *
+ * @param element
+ */
+ public void setMediaElement(MediaElement element) {
+ setElement(element);
+ media = element;
+ }
+
+ public void play() {
+ media.play();
+ }
+
+ public void pause() {
+ media.pause();
+ }
+
+ public void setAltText(String alt) {
+ media.appendChild(Document.get().createTextNode(alt));
+ }
+
+ public void setControls(boolean shouldShowControls) {
+ media.setControls(shouldShowControls);
+ }
+
+ public void setAutoplay(boolean shouldAutoplay) {
+ media.setAutoplay(shouldAutoplay);
+ }
+
+ public void setMuted(boolean mediaMuted) {
+ media.setMuted(mediaMuted);
+ }
+
+ public void addSource(String sourceUrl, String sourceType) {
+ Element src = Document.get().createElement("source").cast();
+ src.setAttribute("src", sourceUrl);
+ src.setAttribute("type", sourceType);
+ media.appendChild(src);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java b/client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java
new file mode 100644
index 0000000000..d5ae77b442
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/VOverlay.java
@@ -0,0 +1,555 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.dom.client.IFrameElement;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.BorderStyle;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+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.BrowserInfo;
+
+/**
+ * In Vaadin 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 VWindow objects.
+ */
+public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> {
+
+ public static class PositionAndSize {
+ private int left, top, width, height;
+
+ public int getLeft() {
+ return left;
+ }
+
+ public void setLeft(int left) {
+ this.left = left;
+ }
+
+ public int getTop() {
+ return top;
+ }
+
+ public void setTop(int top) {
+ this.top = top;
+ }
+
+ 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;
+ }
+
+ public void setAnimationFromCenterProgress(double progress) {
+ left += (int) (width * (1.0 - progress) / 2.0);
+ top += (int) (height * (1.0 - progress) / 2.0);
+ width = (int) (width * progress);
+ height = (int) (height * progress);
+ }
+ }
+
+ /*
+ * The z-index value from where all overlays live. This can be overridden in
+ * any extending class.
+ */
+ public static int Z_INDEX = 20000;
+
+ private static int leftFix = -1;
+
+ private static int topFix = -1;
+
+ /*
+ * 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 = "v-shadow";
+
+ /*
+ * The shadow element for this overlay.
+ */
+ private Element shadow;
+
+ /*
+ * Creator of VOverlow (widget that made the instance, not the layout
+ * parent)
+ */
+ private Widget owner;
+
+ /**
+ * The shim iframe behind the overlay, allowing PDFs and applets to be
+ * covered by overlays.
+ */
+ private IFrameElement shimElement;
+
+ /**
+ * The HTML snippet that is used to render the actual shadow. In consists of
+ * nine different DIV-elements with the following class names:
+ *
+ * <pre>
+ * .v-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>";
+
+ /**
+ * Matches {@link PopupPanel}.ANIMATION_DURATION
+ */
+ private static final int POPUP_PANEL_ANIMATION_DURATION = 200;
+
+ private boolean sinkShadowEvents = false;
+
+ public VOverlay() {
+ super();
+ adjustZIndex();
+ }
+
+ public VOverlay(boolean autoHide) {
+ super(autoHide);
+ adjustZIndex();
+ }
+
+ public VOverlay(boolean autoHide, boolean modal) {
+ super(autoHide, modal);
+ adjustZIndex();
+ }
+
+ public VOverlay(boolean autoHide, boolean modal, boolean showShadow) {
+ super(autoHide, modal);
+ setShadowEnabled(showShadow);
+ adjustZIndex();
+ }
+
+ /**
+ * Method to controle whether DOM elements for shadow are added. With this
+ * method subclasses can control displaying of shadow also after the
+ * constructor.
+ *
+ * @param enabled
+ * true if shadow should be displayed
+ */
+ protected void setShadowEnabled(boolean enabled) {
+ if (enabled != isShadowEnabled()) {
+ if (enabled) {
+ shadow = DOM.createDiv();
+ shadow.setClassName(CLASSNAME_SHADOW);
+ shadow.setInnerHTML(SHADOW_HTML);
+ DOM.setStyleAttribute(shadow, "position", "absolute");
+ addCloseHandler(this);
+ } else {
+ removeShadowIfPresent();
+ shadow = null;
+ }
+ }
+ }
+
+ protected boolean isShadowEnabled() {
+ return shadow != null;
+ }
+
+ private void removeShim() {
+ if (shimElement != null) {
+ shimElement.removeFromParent();
+ }
+ }
+
+ private void removeShadowIfPresent() {
+ if (isShadowAttached()) {
+ shadow.removeFromParent();
+
+ // Remove event listener from the shadow
+ unsinkShadowEvents();
+ }
+ }
+
+ private boolean isShadowAttached() {
+ return isShadowEnabled() && shadow.getParentElement() != null;
+ }
+
+ private boolean isShimAttached() {
+ return shimElement != null && shimElement.hasParentElement();
+ }
+
+ 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 (isShadowEnabled()) {
+ DOM.setStyleAttribute(shadow, "zIndex", "" + zIndex);
+ }
+ }
+
+ @Override
+ public void setPopupPosition(int left, int top) {
+ // TODO, this should in fact be part of
+ // Document.get().getBodyOffsetLeft/Top(). Would require overriding DOM
+ // for all permutations. Now adding fix as margin instead of fixing
+ // left/top because parent class saves the position.
+ Style style = getElement().getStyle();
+ style.setMarginLeft(-adjustByRelativeLeftBodyMargin(), Unit.PX);
+ style.setMarginTop(-adjustByRelativeTopBodyMargin(), Unit.PX);
+ super.setPopupPosition(left, top);
+ sizeOrPositionUpdated(isAnimationEnabled() ? 0 : 1);
+ }
+
+ private IFrameElement getShimElement() {
+ if (shimElement == null) {
+ shimElement = Document.get().createIFrameElement();
+
+ // Insert shim iframe before the main overlay element. It does not
+ // matter if it is in front or behind the shadow as we cannot put a
+ // shim behind the shadow due to its transparency.
+ shimElement.getStyle().setPosition(Position.ABSOLUTE);
+ shimElement.getStyle().setBorderStyle(BorderStyle.NONE);
+ shimElement.setTabIndex(-1);
+ shimElement.setFrameBorder(0);
+ shimElement.setMarginHeight(0);
+ }
+ return shimElement;
+ }
+
+ private int getActualTop() {
+ int y = getAbsoluteTop();
+
+ /* This is needed for IE7 at least */
+ // Account for the difference between absolute position and the
+ // body's positioning context.
+ y -= Document.get().getBodyOffsetTop();
+ y -= adjustByRelativeTopBodyMargin();
+
+ return y;
+ }
+
+ private int getActualLeft() {
+ int x = getAbsoluteLeft();
+
+ /* This is needed for IE7 at least */
+ // Account for the difference between absolute position and the
+ // body's positioning context.
+ x -= Document.get().getBodyOffsetLeft();
+ x -= adjustByRelativeLeftBodyMargin();
+
+ return x;
+ }
+
+ private static int adjustByRelativeTopBodyMargin() {
+ if (topFix == -1) {
+ topFix = detectRelativeBodyFixes("top");
+ }
+ return topFix;
+ }
+
+ private native static int detectRelativeBodyFixes(String axis)
+ /*-{
+ try {
+ var b = $wnd.document.body;
+ var cstyle = b.currentStyle ? b.currentStyle : getComputedStyle(b);
+ if(cstyle && cstyle.position == 'relative') {
+ return b.getBoundingClientRect()[axis];
+ }
+ } catch(e){}
+ return 0;
+ }-*/;
+
+ private static int adjustByRelativeLeftBodyMargin() {
+ if (leftFix == -1) {
+ leftFix = detectRelativeBodyFixes("left");
+
+ }
+ return leftFix;
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ if (isAnimationEnabled()) {
+ new ResizeAnimation().run(POPUP_PANEL_ANIMATION_DURATION);
+ } else {
+ sizeOrPositionUpdated(1.0);
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+
+ // Always ensure shadow is removed when the overlay is removed.
+ removeShadowIfPresent();
+ removeShim();
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (isShadowEnabled()) {
+ shadow.getStyle().setProperty("visibility",
+ visible ? "visible" : "hidden");
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ sizeOrPositionUpdated(1.0);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ sizeOrPositionUpdated(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=='v-shadow-foobar'.
+ */
+ protected void setShadowStyle(String style) {
+ if (isShadowEnabled()) {
+ 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).
+ *
+ */
+ public void sizeOrPositionUpdated() {
+ sizeOrPositionUpdated(1.0);
+ }
+
+ /**
+ * Recalculates proper position and dimensions for the shadow and shim
+ * elements. Can be used to animate the related elements, 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 sizeOrPositionUpdated(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.vaadin.com #2011)
+ zIndex = DOM.getStyleAttribute(getElement(), "zIndex");
+ } catch (Exception ignore) {
+ // Ignored, will cause no harm
+ zIndex = "1000";
+ }
+ if (zIndex == null) {
+ zIndex = "" + Z_INDEX;
+ }
+ // Calculate position and size
+ if (BrowserInfo.get().isIE()) {
+ // Shake IE
+ getOffsetHeight();
+ getOffsetWidth();
+ }
+
+ PositionAndSize positionAndSize = new PositionAndSize();
+ positionAndSize.left = getActualLeft();
+ positionAndSize.top = getActualTop();
+ positionAndSize.width = getOffsetWidth();
+ positionAndSize.height = getOffsetHeight();
+
+ if (positionAndSize.width < 0) {
+ positionAndSize.width = 0;
+ }
+ if (positionAndSize.height < 0) {
+ positionAndSize.height = 0;
+ }
+
+ // Animate the size
+ positionAndSize.setAnimationFromCenterProgress(progress);
+
+ // Opera needs some shaking to get parts of the shadow showing
+ // properly
+ // (ticket #2704)
+ if (BrowserInfo.get().isOpera() && isShadowEnabled()) {
+ // 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
+ if (isShadowEnabled()) {
+ updateSizeAndPosition(shadow, positionAndSize);
+ DOM.setStyleAttribute(shadow, "zIndex", zIndex);
+ DOM.setStyleAttribute(shadow, "display", progress < 0.9 ? "none"
+ : "");
+ }
+ updateSizeAndPosition((Element) Element.as(getShimElement()),
+ positionAndSize);
+
+ // Opera fix, part 2 (ticket #2704)
+ if (BrowserInfo.get().isOpera() && isShadowEnabled()) {
+ // 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 (isShadowEnabled() && !isShadowAttached()) {
+ RootPanel.get().getElement().insertBefore(shadow, getElement());
+ sinkShadowEvents();
+ }
+ if (!isShimAttached()) {
+ RootPanel.get().getElement()
+ .insertBefore(shimElement, getElement());
+ }
+
+ }
+
+ private void updateSizeAndPosition(Element e,
+ PositionAndSize positionAndSize) {
+ e.getStyle().setLeft(positionAndSize.left, Unit.PX);
+ e.getStyle().setTop(positionAndSize.top, Unit.PX);
+ e.getStyle().setWidth(positionAndSize.width, Unit.PX);
+ e.getStyle().setHeight(positionAndSize.height, Unit.PX);
+ }
+
+ protected class ResizeAnimation extends Animation {
+ @Override
+ protected void onUpdate(double progress) {
+ sizeOrPositionUpdated(progress);
+ }
+ }
+
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ removeShadowIfPresent();
+ }
+
+ @Override
+ public void sinkEvents(int eventBitsToAdd) {
+ super.sinkEvents(eventBitsToAdd);
+ // Also sink events on the shadow if present
+ sinkShadowEvents();
+ }
+
+ private void sinkShadowEvents() {
+ if (isSinkShadowEvents() && isShadowAttached()) {
+ // Sink the same events as the actual overlay has sunk
+ DOM.sinkEvents(shadow, DOM.getEventsSunk(getElement()));
+ // Send events to VOverlay.onBrowserEvent
+ DOM.setEventListener(shadow, this);
+ }
+ }
+
+ private void unsinkShadowEvents() {
+ if (isShadowAttached()) {
+ DOM.setEventListener(shadow, null);
+ DOM.sinkEvents(shadow, 0);
+ }
+ }
+
+ /**
+ * Enables or disables sinking the events of the shadow to the same
+ * onBrowserEvent as events to the actual overlay goes.
+ *
+ * Please note, that if you enable this, you can't assume that e.g.
+ * event.getEventTarget returns an element inside the DOM structure of the
+ * overlay
+ *
+ * @param sinkShadowEvents
+ */
+ protected void setSinkShadowEvents(boolean sinkShadowEvents) {
+ this.sinkShadowEvents = sinkShadowEvents;
+ if (sinkShadowEvents) {
+ sinkShadowEvents();
+ } else {
+ unsinkShadowEvents();
+ }
+ }
+
+ protected boolean isSinkShadowEvents() {
+ return sinkShadowEvents;
+ }
+
+ /**
+ * Get owner (Widget that made this VOverlay, not the layout parent) of
+ * VOverlay
+ *
+ * @return Owner (creator) or null if not defined
+ */
+ public Widget getOwner() {
+ return owner;
+ }
+
+ /**
+ * Set owner (Widget that made this VOverlay, not the layout parent) of
+ * VOverlay
+ *
+ * @param owner
+ * Owner (creator) of VOverlay
+ */
+ public void setOwner(Widget owner) {
+ this.owner = owner;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java b/client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java
new file mode 100644
index 0000000000..7bcdcec660
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/VUnknownComponent.java
@@ -0,0 +1,28 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.vaadin.terminal.gwt.client.SimpleTree;
+
+public class VUnknownComponent extends Composite {
+
+ com.google.gwt.user.client.ui.Label caption = new com.google.gwt.user.client.ui.Label();;
+ SimpleTree uidlTree;
+ protected VerticalPanel panel;
+
+ public VUnknownComponent() {
+ panel = new VerticalPanel();
+ panel.add(caption);
+ initWidget(panel);
+ setStyleName("vaadin-unknown");
+ caption.setStyleName("vaadin-unknown-caption");
+ }
+
+ public void setCaption(String c) {
+ caption.getElement().setInnerHTML(c);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java b/client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java
new file mode 100644
index 0000000000..f2f0125c86
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/Vaadin6Connector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public abstract class Vaadin6Connector extends AbstractComponentConnector
+ implements Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ ((Paintable) getWidget()).updateFromUIDL(uidl, client);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java
new file mode 100644
index 0000000000..188d464b84
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/AbsoluteLayoutConnector.java
@@ -0,0 +1,218 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.absolutelayout;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.LayoutClickRpc;
+import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutServerRpc;
+import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.absolutelayout.VAbsoluteLayout.AbsoluteWrapper;
+import com.vaadin.ui.AbsoluteLayout;
+
+@Connect(AbsoluteLayout.class)
+public class AbsoluteLayoutConnector extends
+ AbstractComponentContainerConnector implements DirectionalManagedLayout {
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return getConnectorForElement(element);
+ }
+
+ @Override
+ protected LayoutClickRpc getLayoutClickRPC() {
+ return rpc;
+ };
+
+ };
+
+ private AbsoluteLayoutServerRpc rpc;
+
+ private Map<String, AbsoluteWrapper> connectorIdToComponentWrapper = new HashMap<String, AbsoluteWrapper>();
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(AbsoluteLayoutServerRpc.class, this);
+ }
+
+ /**
+ * Returns the deepest nested child component which contains "element". The
+ * child component is also returned if "element" is part of its caption.
+ *
+ * @param element
+ * An element that is a nested sub element of the root element in
+ * this layout
+ * @return The Paintable which the element is a part of. Null if the element
+ * belongs to the layout and not to a child.
+ */
+ protected ComponentConnector getConnectorForElement(Element element) {
+ return Util.getConnectorForElement(getConnection(), getWidget(),
+ element);
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ VAbsoluteLayout absoluteLayoutWidget = getWidget();
+ AbsoluteWrapper componentWrapper = getWrapper(component);
+
+ boolean captionIsNeeded = VCaption.isNeeded(component.getState());
+
+ VCaption caption = componentWrapper.getCaption();
+
+ if (captionIsNeeded) {
+ if (caption == null) {
+ caption = new VCaption(component, getConnection());
+ absoluteLayoutWidget.add(caption);
+ componentWrapper.setCaption(caption);
+ }
+ caption.updateCaption();
+ componentWrapper.updateCaptionPosition();
+ } else {
+ if (caption != null) {
+ caption.removeFromParent();
+ }
+ }
+
+ }
+
+ @Override
+ public VAbsoluteLayout getWidget() {
+ return (VAbsoluteLayout) super.getWidget();
+ }
+
+ @Override
+ public AbsoluteLayoutState getState() {
+ return (AbsoluteLayoutState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ clickEventHandler.handleEventHandlerRegistration();
+
+ // TODO Margin handling
+
+ for (ComponentConnector child : getChildComponents()) {
+ getWrapper(child).setPosition(
+ getState().getConnectorPosition(child));
+ }
+ };
+
+ private AbsoluteWrapper getWrapper(ComponentConnector child) {
+ String childId = child.getConnectorId();
+ AbsoluteWrapper wrapper = connectorIdToComponentWrapper.get(childId);
+ if (wrapper != null) {
+ return wrapper;
+ }
+
+ wrapper = new AbsoluteWrapper(child.getWidget());
+ connectorIdToComponentWrapper.put(childId, wrapper);
+ getWidget().add(wrapper);
+ return wrapper;
+
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ for (ComponentConnector child : getChildComponents()) {
+ getWrapper(child);
+ }
+
+ for (ComponentConnector oldChild : event.getOldChildren()) {
+ if (oldChild.getParent() != this) {
+ String connectorId = oldChild.getConnectorId();
+ AbsoluteWrapper absoluteWrapper = connectorIdToComponentWrapper
+ .remove(connectorId);
+ absoluteWrapper.destroy();
+ }
+ }
+ }
+
+ @Override
+ public void layoutVertically() {
+ VAbsoluteLayout layout = getWidget();
+ for (ComponentConnector paintable : getChildComponents()) {
+ Widget widget = paintable.getWidget();
+ AbsoluteWrapper wrapper = (AbsoluteWrapper) widget.getParent();
+ Style wrapperStyle = wrapper.getElement().getStyle();
+
+ if (paintable.isRelativeHeight()) {
+ 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 = layout.canvas.getOffsetHeight()
+ - wrapper.getElement().getOffsetTop();
+ }
+ wrapperStyle.setHeight(h, Unit.PX);
+ getLayoutManager().reportHeightAssignedToRelative(paintable, h);
+ } else {
+ wrapperStyle.clearHeight();
+ }
+
+ wrapper.updateCaptionPosition();
+ }
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ VAbsoluteLayout layout = getWidget();
+ for (ComponentConnector paintable : getChildComponents()) {
+ AbsoluteWrapper wrapper = getWrapper(paintable);
+ Style wrapperStyle = wrapper.getElement().getStyle();
+
+ if (paintable.isRelativeWidth()) {
+ 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 = layout.canvas.getOffsetWidth()
+ - wrapper.getElement().getOffsetLeft();
+ }
+ wrapperStyle.setWidth(w, Unit.PX);
+ getLayoutManager().reportWidthAssignedToRelative(paintable, w);
+ } else {
+ wrapperStyle.clearWidth();
+ }
+
+ wrapper.updateCaptionPosition();
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java
new file mode 100644
index 0000000000..e2cb629d68
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/absolutelayout/VAbsoluteLayout.java
@@ -0,0 +1,134 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.absolutelayout;
+
+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.VCaption;
+
+public class VAbsoluteLayout extends ComplexPanel {
+
+ /** Tag name for widget creation */
+ public static final String TAGNAME = "absolutelayout";
+
+ /** Class name, prefix in styling */
+ public static final String CLASSNAME = "v-absolutelayout";
+
+ private DivElement marginElement;
+
+ protected final Element canvas = DOM.createDiv();
+
+ private Object previousStyleName;
+
+ protected ApplicationConnection client;
+
+ public VAbsoluteLayout() {
+ setElement(Document.get().createDivElement());
+ setStyleName(CLASSNAME);
+ marginElement = Document.get().createDivElement();
+ canvas.getStyle().setProperty("position", "relative");
+ canvas.getStyle().setProperty("overflow", "hidden");
+ marginElement.appendChild(canvas);
+ getElement().appendChild(marginElement);
+
+ canvas.setClassName(CLASSNAME + "-canvas");
+ canvas.setClassName(CLASSNAME + "-margin");
+ }
+
+ @Override
+ public void add(Widget child) {
+ super.add(child, canvas);
+ }
+
+ public static class AbsoluteWrapper extends SimplePanel {
+ private String css;
+ String left;
+ String top;
+ String right;
+ String bottom;
+ private String zIndex;
+
+ private VCaption caption;
+
+ public AbsoluteWrapper(Widget child) {
+ setWidget(child);
+ setStyleName(CLASSNAME + "-wrapper");
+ }
+
+ public VCaption getCaption() {
+ return caption;
+ }
+
+ public void setCaption(VCaption caption) {
+ this.caption = caption;
+ }
+
+ public void destroy() {
+ if (caption != null) {
+ caption.removeFromParent();
+ }
+ removeFromParent();
+ }
+
+ 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();
+ /*
+ * IE8 dies when nulling zIndex, even in IE7 mode. All other css
+ * properties (and even in older IE's) accept null values just
+ * fine. Assign empty string instead of null.
+ */
+ if (zIndex != null) {
+ style.setProperty("zIndex", zIndex);
+ } else {
+ style.setProperty("zIndex", "");
+ }
+ style.setProperty("top", top);
+ style.setProperty("left", left);
+ style.setProperty("right", right);
+ style.setProperty("bottom", bottom);
+
+ }
+ updateCaptionPosition();
+ }
+
+ 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());
+ }
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java
new file mode 100644
index 0000000000..7ff1257da3
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/AccordionConnector.java
@@ -0,0 +1,78 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.accordion;
+
+import java.util.Iterator;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.accordion.VAccordion.StackItem;
+import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector;
+import com.vaadin.ui.Accordion;
+
+@Connect(Accordion.class)
+public class AccordionConnector extends TabsheetBaseConnector implements
+ SimpleManagedLayout, MayScrollChildren {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().selectedUIDLItemIndex = -1;
+ super.updateFromUIDL(uidl, client);
+ /*
+ * Render content after all tabs have been created and we know how large
+ * the content area is
+ */
+ if (getWidget().selectedUIDLItemIndex >= 0) {
+ StackItem selectedItem = getWidget().getStackItem(
+ getWidget().selectedUIDLItemIndex);
+ UIDL selectedTabUIDL = getWidget().lazyUpdateMap
+ .remove(selectedItem);
+ getWidget().open(getWidget().selectedUIDLItemIndex);
+
+ selectedItem.setContent(selectedTabUIDL);
+ } else if (isRealUpdate(uidl) && getWidget().openTab != null) {
+ getWidget().close(getWidget().openTab);
+ }
+
+ getWidget().iLayout();
+ // finally render possible hidden tabs
+ if (getWidget().lazyUpdateMap.size() > 0) {
+ for (Iterator iterator = getWidget().lazyUpdateMap.keySet()
+ .iterator(); iterator.hasNext();) {
+ StackItem item = (StackItem) iterator.next();
+ item.setContent(getWidget().lazyUpdateMap.get(item));
+ }
+ getWidget().lazyUpdateMap.clear();
+ }
+
+ }
+
+ @Override
+ public VAccordion getWidget() {
+ return (VAccordion) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ /* Accordion does not render its children's captions */
+ }
+
+ @Override
+ public void layout() {
+ VAccordion accordion = getWidget();
+
+ accordion.updateOpenTabSize();
+
+ if (isUndefinedHeight()) {
+ accordion.openTab.setHeightFromWidget();
+ }
+ accordion.iLayout();
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java
new file mode 100644
index 0000000000..d9320787e8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/accordion/VAccordion.java
@@ -0,0 +1,515 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.accordion;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheetBase;
+
+public class VAccordion extends VTabsheetBase {
+
+ public static final String CLASSNAME = "v-accordion";
+
+ private Set<Widget> widgets = new HashSet<Widget>();
+
+ HashMap<StackItem, UIDL> lazyUpdateMap = new HashMap<StackItem, UIDL>();
+
+ StackItem openTab = null;
+
+ int selectedUIDLItemIndex = -1;
+
+ private final TouchScrollHandler touchScrollHandler;
+
+ public VAccordion() {
+ super(CLASSNAME);
+ touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
+ }
+
+ @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;
+ ComponentConnector 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) {
+ oldWidget.removeFromParent();
+ }
+ }
+ return item;
+ }
+
+ 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();
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Sets the size of the open tab
+ */
+ void updateOpenTabSize() {
+ if (openTab == null) {
+ return;
+ }
+
+ // WIDTH
+ if (!isDynamicWidth()) {
+ openTab.setWidth("100%");
+ } else {
+ openTab.setWidth(null);
+ }
+
+ // 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;
+ }
+
+ openTab.setHeight(spaceForOpenItem);
+ } else {
+ 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);
+ }
+ }
+
+ /**
+ * A StackItem has always two children, Child 0 is a VCaption, Child 1 is
+ * the actual child widget.
+ */
+ protected class StackItem extends ComplexPanel implements ClickHandler {
+
+ 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 widget = getChildWidget();
+ if (widget == null) {
+ return;
+ }
+
+ int paintableHeight = widget.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 VCaption caption;
+ private boolean open = false;
+ private Element content = DOM.createDiv();
+ private Element captionNode = DOM.createDiv();
+
+ public StackItem(UIDL tabUidl) {
+ setElement(DOM.createDiv());
+ caption = new VCaption(client);
+ caption.addClickHandler(this);
+ super.add(caption, captionNode);
+ DOM.appendChild(captionNode, caption.getElement());
+ DOM.appendChild(getElement(), captionNode);
+ DOM.appendChild(getElement(), content);
+
+ getElement().addClassName(CLASSNAME + "-item");
+ captionNode.addClassName(CLASSNAME + "-item-caption");
+ content.addClassName(CLASSNAME + "-item-content");
+
+ touchScrollHandler.addElement(getContainerElement());
+
+ close();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ onSelectTab(this);
+ }
+
+ public Element getContainerElement() {
+ return content;
+ }
+
+ public Widget getChildWidget() {
+ if (getWidgetCount() > 1) {
+ return getWidget(1);
+ } else {
+ return null;
+ }
+ }
+
+ public void replaceWidget(Widget newWidget) {
+ if (getWidgetCount() > 1) {
+ Widget oldWidget = getWidget(1);
+ ComponentConnector oldPaintable = ConnectorMap.get(client)
+ .getConnector(oldWidget);
+ ConnectorMap.get(client).unregisterConnector(oldPaintable);
+ widgets.remove(oldWidget);
+ remove(1);
+ }
+ add(newWidget, content);
+ widgets.add(newWidget);
+ }
+
+ 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);
+ setWidth("");
+ open = false;
+ }
+
+ public boolean isOpen() {
+ return open;
+ }
+
+ public void setContent(UIDL contentUidl) {
+ final ComponentConnector newPntbl = client
+ .getPaintable(contentUidl);
+ Widget newWidget = newPntbl.getWidget();
+ if (getChildWidget() == null) {
+ add(newWidget, content);
+ widgets.add(newWidget);
+ } else if (getChildWidget() != newWidget) {
+ replaceWidget(newWidget);
+ }
+ if (contentUidl.getBooleanAttribute("cached")) {
+ /*
+ * The size of a cached, relative sized component must be
+ * updated to report correct size.
+ */
+ client.handleComponentRelativeSize(newPntbl.getWidget());
+ }
+ if (isOpen() && isDynamicHeight()) {
+ setHeightFromWidget();
+ }
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ onSelectTab(this);
+ }
+
+ public void updateCaption(UIDL uidl) {
+ // TODO need to call this because the caption does not have an owner
+ caption.updateCaptionWithoutOwner(
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE),
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON));
+ }
+
+ public int getWidgetWidth() {
+ return DOM.getFirstChild(content).getOffsetWidth();
+ }
+
+ public boolean contains(ComponentConnector p) {
+ return (getChildWidget() == p.getWidget());
+ }
+
+ public boolean isCaptionVisible() {
+ return caption.isVisible();
+ }
+
+ }
+
+ @Override
+ protected void clearPaintables() {
+ clear();
+ }
+
+ boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ boolean isDynamicHeight() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedHeight();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Iterator<Widget> getWidgetIterator() {
+ return widgets.iterator();
+ }
+
+ @Override
+ protected int getTabCount() {
+ return getWidgetCount();
+ }
+
+ @Override
+ protected void removeTab(int index) {
+ StackItem item = getStackItem(index);
+ remove(item);
+ touchScrollHandler.removeElement(item.getContainerElement());
+ }
+
+ @Override
+ protected ComponentConnector getTab(int index) {
+ if (index < getWidgetCount()) {
+ Widget w = getStackItem(index);
+ return ConnectorMap.get(client).getConnector(w);
+ }
+
+ return null;
+ }
+
+ StackItem getStackItem(int index) {
+ return (StackItem) getWidget(index);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java
new file mode 100644
index 0000000000..6b713c229c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/audio/AudioConnector.java
@@ -0,0 +1,45 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.audio;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.MediaBaseConnector;
+import com.vaadin.ui.Audio;
+
+@Connect(Audio.class)
+public class AudioConnector extends MediaBaseConnector {
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ Style style = getWidget().getElement().getStyle();
+
+ // Make sure that the controls are not clipped if visible.
+ if (getState().isShowControls()
+ && (style.getHeight() == null || "".equals(style.getHeight()))) {
+ if (BrowserInfo.get().isChrome()) {
+ style.setHeight(32, Unit.PX);
+ } else {
+ style.setHeight(25, Unit.PX);
+ }
+ }
+ }
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VAudio.class);
+ }
+
+ @Override
+ protected String getDefaultAltHtml() {
+ return "Your browser does not support the <code>audio</code> element.";
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java b/client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java
new file mode 100644
index 0000000000..08bc95ba16
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/audio/VAudio.java
@@ -0,0 +1,22 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.audio;
+
+import com.google.gwt.dom.client.AudioElement;
+import com.google.gwt.dom.client.Document;
+import com.vaadin.terminal.gwt.client.ui.VMediaBase;
+
+public class VAudio extends VMediaBase {
+ private static String CLASSNAME = "v-audio";
+
+ private AudioElement audio;
+
+ public VAudio() {
+ audio = Document.get().createAudioElement();
+ setMediaElement(audio);
+ setStyleName(CLASSNAME);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java
new file mode 100644
index 0000000000..0cec6ce96b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/button/ButtonConnector.java
@@ -0,0 +1,136 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.button;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.button.ButtonServerRpc;
+import com.vaadin.shared.ui.button.ButtonState;
+import com.vaadin.terminal.gwt.client.EventHelper;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.ui.Button;
+
+@Connect(value = Button.class, loadStyle = LoadStyle.EAGER)
+public class ButtonConnector extends AbstractComponentConnector implements
+ BlurHandler, FocusHandler, ClickHandler {
+
+ private ButtonServerRpc rpc = RpcProxy.create(ButtonServerRpc.class, this);
+ private FocusAndBlurServerRpc focusBlurProxy = RpcProxy.create(
+ FocusAndBlurServerRpc.class, this);
+
+ private HandlerRegistration focusHandlerRegistration = null;
+ private HandlerRegistration blurHandlerRegistration = null;
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ getWidget().addClickHandler(this);
+ getWidget().client = getConnection();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ focusHandlerRegistration = EventHelper.updateFocusHandler(this,
+ focusHandlerRegistration);
+ blurHandlerRegistration = EventHelper.updateBlurHandler(this,
+ blurHandlerRegistration);
+ // Set text
+ if (getState().isHtmlContentAllowed()) {
+ getWidget().setHtml(getState().getCaption());
+ } else {
+ getWidget().setText(getState().getCaption());
+ }
+
+ // handle error
+ if (null != getState().getErrorMessage()) {
+ if (getWidget().errorIndicatorElement == null) {
+ getWidget().errorIndicatorElement = DOM.createSpan();
+ getWidget().errorIndicatorElement
+ .setClassName("v-errorindicator");
+ }
+ getWidget().wrapper.insertBefore(getWidget().errorIndicatorElement,
+ getWidget().captionElement);
+
+ } else if (getWidget().errorIndicatorElement != null) {
+ getWidget().wrapper.removeChild(getWidget().errorIndicatorElement);
+ getWidget().errorIndicatorElement = null;
+ }
+
+ if (getState().getIcon() != null) {
+ if (getWidget().icon == null) {
+ getWidget().icon = new Icon(getConnection());
+ getWidget().wrapper.insertBefore(getWidget().icon.getElement(),
+ getWidget().captionElement);
+ }
+ getWidget().icon.setUri(getState().getIcon().getURL());
+ } else {
+ if (getWidget().icon != null) {
+ getWidget().wrapper.removeChild(getWidget().icon.getElement());
+ getWidget().icon = null;
+ }
+ }
+
+ getWidget().clickShortcut = getState().getClickShortcutKeyCode();
+ }
+
+ @Override
+ public VButton getWidget() {
+ return (VButton) super.getWidget();
+ }
+
+ @Override
+ public ButtonState getState() {
+ return (ButtonState) super.getState();
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ // EventHelper.updateFocusHandler ensures that this is called only when
+ // there is a listener on server side
+ focusBlurProxy.focus();
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ // EventHelper.updateFocusHandler ensures that this is called only when
+ // there is a listener on server side
+ focusBlurProxy.blur();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (getState().isDisableOnClick()) {
+ getWidget().setEnabled(false);
+ rpc.disableOnClick();
+ }
+
+ // Add mouse details
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event.getNativeEvent(), getWidget()
+ .getElement());
+ rpc.click(details);
+
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java b/client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java
new file mode 100644
index 0000000000..3232338f62
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/button/VButton.java
@@ -0,0 +1,419 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.button;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Accessibility;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+
+public class VButton extends FocusWidget implements ClickHandler {
+
+ public static final String CLASSNAME = "v-button";
+ private static final String CLASSNAME_PRESSED = "v-pressed";
+
+ // mouse movement is checked before synthesizing click event on mouseout
+ protected static int MOVE_THRESHOLD = 3;
+ protected int mousedownX = 0;
+ protected int mousedownY = 0;
+
+ protected ApplicationConnection client;
+
+ protected final Element wrapper = DOM.createSpan();
+
+ protected Element errorIndicatorElement;
+
+ protected final Element captionElement = DOM.createSpan();
+
+ protected Icon icon;
+
+ /**
+ * Helper flag to handle special-case where the button is moved from under
+ * mouse while clicking it. In this case mouse leaves the button without
+ * moving.
+ */
+ protected boolean clickPending;
+
+ private boolean enabled = true;
+
+ private int tabIndex = 0;
+
+ /*
+ * BELOW PRIVATE MEMBERS COPY-PASTED FROM GWT CustomButton
+ */
+
+ /**
+ * If <code>true</code>, this widget is capturing with the mouse held down.
+ */
+ private boolean isCapturing;
+
+ /**
+ * If <code>true</code>, this widget has focus with the space bar down. This
+ * means that we will get events when the button is released, but we should
+ * trigger the button only if the button is still focused at that point.
+ */
+ private boolean isFocusing;
+
+ /**
+ * Used to decide whether to allow clicks to propagate up to the superclass
+ * or container elements.
+ */
+ private boolean disallowNextClick = false;
+ private boolean isHovering;
+
+ protected int clickShortcut = 0;
+
+ private HandlerRegistration focusHandlerRegistration;
+ private HandlerRegistration blurHandlerRegistration;
+
+ /**
+ * If caption should be rendered in HTML
+ */
+ protected boolean htmlCaption = false;
+
+ public VButton() {
+ super(DOM.createDiv());
+ setTabIndex(0);
+ sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.FOCUSEVENTS
+ | Event.KEYEVENTS);
+
+ setStyleName(CLASSNAME);
+
+ // Add a11y role "button"
+ Accessibility.setRole(getElement(), Accessibility.ROLE_BUTTON);
+
+ wrapper.setClassName(getStylePrimaryName() + "-wrap");
+ getElement().appendChild(wrapper);
+ captionElement.setClassName(getStylePrimaryName() + "-caption");
+ wrapper.appendChild(captionElement);
+
+ addClickHandler(this);
+ }
+
+ public void setText(String text) {
+ captionElement.setInnerText(text);
+ }
+
+ public void setHtml(String html) {
+ captionElement.setInnerHTML(html);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ /*
+ * Copy-pasted from GWT CustomButton, some minor modifications done:
+ *
+ * -for IE/Opera added CLASSNAME_PRESSED
+ *
+ * -event.preventDefault() commented from ONMOUSEDOWN (Firefox won't apply
+ * :active styles if it is present)
+ *
+ * -Tooltip event handling added
+ *
+ * -onload event handler added (for icon handling)
+ */
+ public void onBrowserEvent(Event event) {
+ if (DOM.eventGetType(event) == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ // Should not act on button if disabled.
+ if (!isEnabled()) {
+ // This can happen when events are bubbled up from non-disabled
+ // children
+ return;
+ }
+
+ int type = DOM.eventGetType(event);
+ switch (type) {
+ case Event.ONCLICK:
+ // If clicks are currently disallowed, keep it from bubbling or
+ // being passed to the superclass.
+ if (disallowNextClick) {
+ event.stopPropagation();
+ disallowNextClick = false;
+ return;
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
+ // This was moved from mouseover, which iOS sometimes skips.
+ // We're certainly hovering at this point, and we don't actually
+ // need that information before this point.
+ setHovering(true);
+ }
+ if (event.getButton() == Event.BUTTON_LEFT) {
+ // save mouse position to detect movement before synthesizing
+ // event later
+ mousedownX = event.getClientX();
+ mousedownY = event.getClientY();
+
+ disallowNextClick = true;
+ clickPending = true;
+ setFocus(true);
+ DOM.setCapture(getElement());
+ isCapturing = true;
+ // Prevent dragging (on some browsers);
+ // DOM.eventPreventDefault(event);
+ if (BrowserInfo.get().isIE() || BrowserInfo.get().isOpera()) {
+ addStyleName(CLASSNAME_PRESSED);
+ }
+ }
+ break;
+ case Event.ONMOUSEUP:
+ if (isCapturing) {
+ isCapturing = false;
+ DOM.releaseCapture(getElement());
+ if (isHovering() && event.getButton() == Event.BUTTON_LEFT) {
+ // Click ok
+ disallowNextClick = false;
+ }
+ if (BrowserInfo.get().isIE() || BrowserInfo.get().isOpera()) {
+ removeStyleName(CLASSNAME_PRESSED);
+ }
+ // Explicitly prevent IE 8 from propagating mouseup events
+ // upward (fixes #6753)
+ if (BrowserInfo.get().isIE8()) {
+ event.stopPropagation();
+ }
+ }
+ break;
+ case Event.ONMOUSEMOVE:
+ clickPending = false;
+ if (isCapturing) {
+ // Prevent dragging (on other browsers);
+ DOM.eventPreventDefault(event);
+ }
+ break;
+ case Event.ONMOUSEOUT:
+ Element to = event.getRelatedTarget();
+ if (getElement().isOrHasChild(DOM.eventGetTarget(event))
+ && (to == null || !getElement().isOrHasChild(to))) {
+ if (clickPending
+ && Math.abs(mousedownX - event.getClientX()) < MOVE_THRESHOLD
+ && Math.abs(mousedownY - event.getClientY()) < MOVE_THRESHOLD) {
+ onClick();
+ break;
+ }
+ clickPending = false;
+ if (isCapturing) {
+ }
+ setHovering(false);
+ if (BrowserInfo.get().isIE() || BrowserInfo.get().isOpera()) {
+ removeStyleName(CLASSNAME_PRESSED);
+ }
+ }
+ break;
+ case Event.ONBLUR:
+ if (isFocusing) {
+ isFocusing = false;
+ }
+ break;
+ case Event.ONLOSECAPTURE:
+ if (isCapturing) {
+ isCapturing = false;
+ }
+ break;
+ }
+
+ super.onBrowserEvent(event);
+
+ // Synthesize clicks based on keyboard events AFTER the normal key
+ // handling.
+ if ((event.getTypeInt() & Event.KEYEVENTS) != 0) {
+ switch (type) {
+ case Event.ONKEYDOWN:
+ // Stop propagation when the user starts pressing a button that
+ // we are handling to prevent actions from getting triggered
+ if (event.getKeyCode() == 32 /* space */) {
+ isFocusing = true;
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getKeyCode() == KeyCodes.KEY_ENTER) {
+ event.stopPropagation();
+ }
+ break;
+ case Event.ONKEYUP:
+ if (isFocusing && event.getKeyCode() == 32 /* space */) {
+ isFocusing = false;
+ onClick();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ break;
+ case Event.ONKEYPRESS:
+ if (event.getKeyCode() == KeyCodes.KEY_ENTER) {
+ onClick();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ break;
+ }
+ }
+ }
+
+ final void setHovering(boolean hovering) {
+ if (hovering != isHovering()) {
+ isHovering = hovering;
+ }
+ }
+
+ final boolean isHovering() {
+ return isHovering;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event
+ * .dom.client.ClickEvent)
+ */
+ @Override
+ public void onClick(ClickEvent event) {
+ if (BrowserInfo.get().isSafari()) {
+ VButton.this.setFocus(true);
+ }
+
+ clickPending = false;
+ }
+
+ /*
+ * ALL BELOW COPY-PASTED FROM GWT CustomButton
+ */
+
+ /**
+ * Called internally when the user finishes clicking on this button. The
+ * default behavior is to fire the click event to listeners. Subclasses that
+ * override {@link #onClickStart()} should override this method to restore
+ * the normal widget display.
+ * <p>
+ * To add custom code for a click event, override
+ * {@link #onClick(ClickEvent)} instead of this.
+ */
+ protected void onClick() {
+ // Allow the click we're about to synthesize to pass through to the
+ // superclass and containing elements. Element.dispatchEvent() is
+ // synchronous, so we simply set and clear the flag within this method.
+
+ disallowNextClick = false;
+
+ // Mouse coordinates are not always available (e.g., when the click is
+ // caused by a keyboard event).
+ NativeEvent evt = Document.get().createClickEvent(1, 0, 0, 0, 0, false,
+ false, false, false);
+ getElement().dispatchEvent(evt);
+ }
+
+ /**
+ * Sets whether this button is enabled.
+ *
+ * @param enabled
+ * <code>true</code> to enable the button, <code>false</code> to
+ * disable it
+ */
+
+ @Override
+ public final void setEnabled(boolean enabled) {
+ if (isEnabled() != enabled) {
+ this.enabled = enabled;
+ if (!enabled) {
+ cleanupCaptureState();
+ Accessibility.removeState(getElement(),
+ Accessibility.STATE_PRESSED);
+ super.setTabIndex(-1);
+ } else {
+ Accessibility.setState(getElement(),
+ Accessibility.STATE_PRESSED, "false");
+ super.setTabIndex(tabIndex);
+ }
+ }
+ }
+
+ @Override
+ public final boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public final void setTabIndex(int index) {
+ super.setTabIndex(index);
+ tabIndex = index;
+ }
+
+ /**
+ * Resets internal state if this button can no longer service events. This
+ * can occur when the widget becomes detached or disabled.
+ */
+ private void cleanupCaptureState() {
+ if (isCapturing || isFocusing) {
+ DOM.releaseCapture(getElement());
+ isCapturing = false;
+ isFocusing = false;
+ }
+ }
+
+ private static native int getHorizontalBorderAndPaddingWidth(Element elem)
+ /*-{
+ // THIS METHOD IS ONLY USED FOR INTERNET EXPLORER, IT DOESN'T WORK WITH OTHERS
+
+ var convertToPixel = function(elem, value) {
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+ // Remember the original values
+ var left = elem.style.left, rsLeft = elem.runtimeStyle.left;
+
+ // Put in the new values to get a computed value out
+ elem.runtimeStyle.left = elem.currentStyle.left;
+ elem.style.left = value || 0;
+ var ret = elem.style.pixelLeft;
+
+ // Revert the changed values
+ elem.style.left = left;
+ elem.runtimeStyle.left = rsLeft;
+
+ return ret;
+ }
+
+ var ret = 0;
+
+ var sides = ["Right","Left"];
+ for(var i=0; i<2; i++) {
+ var side = sides[i];
+ var value;
+ // Border -------------------------------------------------------
+ if(elem.currentStyle["border"+side+"Style"] != "none") {
+ value = elem.currentStyle["border"+side+"Width"];
+ if ( !/^\d+(px)?$/i.test( value ) && /^\d/.test( value ) ) {
+ ret += convertToPixel(elem, value);
+ } else if(value.length > 2) {
+ ret += parseInt(value.substr(0, value.length-2));
+ }
+ }
+
+ // Padding -------------------------------------------------------
+ value = elem.currentStyle["padding"+side];
+ if ( !/^\d+(px)?$/i.test( value ) && /^\d/.test( value ) ) {
+ ret += convertToPixel(elem, value);
+ } else if(value.length > 2) {
+ ret += parseInt(value.substr(0, value.length-2));
+ }
+ }
+
+ return ret;
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java
new file mode 100644
index 0000000000..c7e827bc74
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/CheckBoxConnector.java
@@ -0,0 +1,146 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.checkbox;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc;
+import com.vaadin.shared.ui.checkbox.CheckBoxState;
+import com.vaadin.terminal.gwt.client.EventHelper;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.ui.CheckBox;
+
+@Connect(CheckBox.class)
+public class CheckBoxConnector extends AbstractFieldConnector implements
+ FocusHandler, BlurHandler, ClickHandler {
+
+ private HandlerRegistration focusHandlerRegistration;
+ private HandlerRegistration blurHandlerRegistration;
+
+ private CheckBoxServerRpc rpc = RpcProxy.create(CheckBoxServerRpc.class,
+ this);
+ private FocusAndBlurServerRpc focusBlurRpc = RpcProxy.create(
+ FocusAndBlurServerRpc.class, this);
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().addClickHandler(this);
+ getWidget().client = getConnection();
+ getWidget().id = getConnectorId();
+
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ focusHandlerRegistration = EventHelper.updateFocusHandler(this,
+ focusHandlerRegistration);
+ blurHandlerRegistration = EventHelper.updateBlurHandler(this,
+ blurHandlerRegistration);
+
+ if (null != getState().getErrorMessage()) {
+ if (getWidget().errorIndicatorElement == null) {
+ getWidget().errorIndicatorElement = DOM.createSpan();
+ getWidget().errorIndicatorElement.setInnerHTML("&nbsp;");
+ DOM.setElementProperty(getWidget().errorIndicatorElement,
+ "className", "v-errorindicator");
+ DOM.appendChild(getWidget().getElement(),
+ getWidget().errorIndicatorElement);
+ DOM.sinkEvents(getWidget().errorIndicatorElement,
+ VTooltip.TOOLTIP_EVENTS | Event.ONCLICK);
+ } else {
+ DOM.setStyleAttribute(getWidget().errorIndicatorElement,
+ "display", "");
+ }
+ } else if (getWidget().errorIndicatorElement != null) {
+ DOM.setStyleAttribute(getWidget().errorIndicatorElement, "display",
+ "none");
+ }
+
+ if (isReadOnly()) {
+ getWidget().setEnabled(false);
+ }
+
+ if (getState().getIcon() != null) {
+ if (getWidget().icon == null) {
+ getWidget().icon = new Icon(getConnection());
+ DOM.insertChild(getWidget().getElement(),
+ getWidget().icon.getElement(), 1);
+ getWidget().icon.sinkEvents(VTooltip.TOOLTIP_EVENTS);
+ getWidget().icon.sinkEvents(Event.ONCLICK);
+ }
+ getWidget().icon.setUri(getState().getIcon().getURL());
+ } else if (getWidget().icon != null) {
+ // detach icon
+ DOM.removeChild(getWidget().getElement(),
+ getWidget().icon.getElement());
+ getWidget().icon = null;
+ }
+
+ // Set text
+ getWidget().setText(getState().getCaption());
+ getWidget().setValue(getState().isChecked());
+ getWidget().immediate = getState().isImmediate();
+ }
+
+ @Override
+ public CheckBoxState getState() {
+ return (CheckBoxState) super.getState();
+ }
+
+ @Override
+ public VCheckBox getWidget() {
+ return (VCheckBox) super.getWidget();
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ // EventHelper.updateFocusHandler ensures that this is called only when
+ // there is a listener on server side
+ focusBlurRpc.focus();
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ // EventHelper.updateFocusHandler ensures that this is called only when
+ // there is a listener on server side
+ focusBlurRpc.blur();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (!isEnabled()) {
+ return;
+ }
+
+ // Add mouse details
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event.getNativeEvent(), getWidget()
+ .getElement());
+ rpc.setChecked(getWidget().getValue(), details);
+
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java
new file mode 100644
index 0000000000..a6eec2de8a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/checkbox/VCheckBox.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.checkbox;
+
+import com.google.gwt.user.client.DOM;
+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.Util;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+
+public class VCheckBox extends com.google.gwt.user.client.ui.CheckBox implements
+ Field {
+
+ public static final String CLASSNAME = "v-checkbox";
+
+ String id;
+
+ boolean immediate;
+
+ ApplicationConnection client;
+
+ Element errorIndicatorElement;
+
+ Icon icon;
+
+ public VCheckBox() {
+ setStyleName(CLASSNAME);
+
+ Element el = DOM.getFirstChild(getElement());
+ while (el != null) {
+ DOM.sinkEvents(el,
+ (DOM.getEventsSunk(el) | VTooltip.TOOLTIP_EVENTS));
+ el = DOM.getNextSibling(el);
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (icon != null && (event.getTypeInt() == Event.ONCLICK)
+ && (DOM.eventGetTarget(event) == icon.getElement())) {
+ // Click on icon should do nothing if widget is disabled
+ if (isEnabled()) {
+ setValue(!getValue());
+ }
+ }
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java
new file mode 100644
index 0000000000..0fa71bb7a6
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/ComboBoxConnector.java
@@ -0,0 +1,241 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.combobox;
+
+import java.util.Iterator;
+
+import com.vaadin.shared.ui.Connect;
+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;
+import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.combobox.VFilterSelect.FilterSelectSuggestion;
+import com.vaadin.terminal.gwt.client.ui.menubar.MenuItem;
+import com.vaadin.ui.Select;
+
+@Connect(Select.class)
+public class ComboBoxConnector extends AbstractFieldConnector implements
+ Paintable, SimpleManagedLayout {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal
+ * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Save details
+ getWidget().client = client;
+ getWidget().paintableId = uidl.getId();
+
+ getWidget().readonly = isReadOnly();
+ getWidget().enabled = isEnabled();
+
+ getWidget().tb.setEnabled(getWidget().enabled);
+ getWidget().updateReadOnly();
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // Inverse logic here to make the default case (text input enabled)
+ // work without additional UIDL messages
+ boolean noTextInput = uidl
+ .hasAttribute(VFilterSelect.ATTR_NO_TEXT_INPUT)
+ && uidl.getBooleanAttribute(VFilterSelect.ATTR_NO_TEXT_INPUT);
+ getWidget().setTextInputEnabled(!noTextInput);
+
+ // not a FocusWidget -> needs own tabindex handling
+ if (uidl.hasAttribute("tabindex")) {
+ getWidget().tb.setTabIndex(uidl.getIntAttribute("tabindex"));
+ }
+
+ if (uidl.hasAttribute("filteringmode")) {
+ getWidget().filteringmode = uidl.getIntAttribute("filteringmode");
+ }
+
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().nullSelectionAllowed = uidl.hasAttribute("nullselect");
+
+ getWidget().nullSelectItem = uidl.hasAttribute("nullselectitem")
+ && uidl.getBooleanAttribute("nullselectitem");
+
+ getWidget().currentPage = uidl.getIntVariable("page");
+
+ if (uidl.hasAttribute("pagelength")) {
+ getWidget().pageLength = uidl.getIntAttribute("pagelength");
+ }
+
+ if (uidl.hasAttribute(VFilterSelect.ATTR_INPUTPROMPT)) {
+ // input prompt changed from server
+ getWidget().inputPrompt = uidl
+ .getStringAttribute(VFilterSelect.ATTR_INPUTPROMPT);
+ } else {
+ getWidget().inputPrompt = "";
+ }
+
+ getWidget().suggestionPopup.updateStyleNames(uidl, getState());
+
+ getWidget().allowNewItem = uidl.hasAttribute("allownewitem");
+ getWidget().lastNewItemString = null;
+
+ getWidget().currentSuggestions.clear();
+ if (!getWidget().waitingForFilteringResponse) {
+ /*
+ * Clear the current suggestions as the server response always
+ * includes the new ones. Exception is when filtering, then we need
+ * to retain the value if the user does not select any of the
+ * options matching the filter.
+ */
+ getWidget().currentSuggestion = null;
+ /*
+ * Also ensure no old items in menu. Unless cleared the old values
+ * may cause odd effects on blur events. Suggestions in menu might
+ * not necessary exist in select at all anymore.
+ */
+ getWidget().suggestionPopup.menu.clearItems();
+
+ }
+
+ final UIDL options = uidl.getChildUIDL(0);
+ if (uidl.hasAttribute("totalMatches")) {
+ getWidget().totalMatches = uidl.getIntAttribute("totalMatches");
+ } else {
+ getWidget().totalMatches = 0;
+ }
+
+ // used only to calculate minimum popup width
+ String captions = Util.escapeHTML(getWidget().inputPrompt);
+
+ for (final Iterator<?> i = options.getChildIterator(); i.hasNext();) {
+ final UIDL optionUidl = (UIDL) i.next();
+ final FilterSelectSuggestion suggestion = getWidget().new FilterSelectSuggestion(
+ optionUidl);
+ getWidget().currentSuggestions.add(suggestion);
+ if (optionUidl.hasAttribute("selected")) {
+ if (!getWidget().waitingForFilteringResponse
+ || getWidget().popupOpenerClicked) {
+ String newSelectedOptionKey = Integer.toString(suggestion
+ .getOptionKey());
+ if (!newSelectedOptionKey
+ .equals(getWidget().selectedOptionKey)
+ || suggestion.getReplacementString().equals(
+ getWidget().tb.getText())) {
+ // Update text field if we've got a new selection
+ // Also update if we've got the same text to retain old
+ // text selection behavior
+ getWidget().setPromptingOff(
+ suggestion.getReplacementString());
+ getWidget().selectedOptionKey = newSelectedOptionKey;
+ }
+ }
+ getWidget().currentSuggestion = suggestion;
+ getWidget().setSelectedItemIcon(suggestion.getIconUri());
+ }
+
+ // Collect captions so we can calculate minimum width for textarea
+ if (captions.length() > 0) {
+ captions += "|";
+ }
+ captions += Util.escapeHTML(suggestion.getReplacementString());
+ }
+
+ if ((!getWidget().waitingForFilteringResponse || getWidget().popupOpenerClicked)
+ && uidl.hasVariable("selected")
+ && uidl.getStringArrayVariable("selected").length == 0) {
+ // select nulled
+ if (!getWidget().waitingForFilteringResponse
+ || !getWidget().popupOpenerClicked) {
+ if (!getWidget().focused) {
+ /*
+ * client.updateComponent overwrites all styles so we must
+ * ALWAYS set the prompting style at this point, even though
+ * we think it has been set already...
+ */
+ getWidget().prompting = false;
+ getWidget().setPromptingOn();
+ } else {
+ // we have focus in field, prompting can't be set on,
+ // instead just clear the input
+ getWidget().tb.setValue("");
+ }
+ }
+ getWidget().setSelectedItemIcon(null);
+ getWidget().selectedOptionKey = null;
+ }
+
+ if (getWidget().waitingForFilteringResponse
+ && getWidget().lastFilter.toLowerCase().equals(
+ uidl.getStringVariable("filter"))) {
+ getWidget().suggestionPopup.showSuggestions(
+ getWidget().currentSuggestions, getWidget().currentPage,
+ getWidget().totalMatches);
+ getWidget().waitingForFilteringResponse = false;
+ if (!getWidget().popupOpenerClicked
+ && getWidget().selectPopupItemWhenResponseIsReceived != VFilterSelect.Select.NONE) {
+ // we're paging w/ arrows
+ if (getWidget().selectPopupItemWhenResponseIsReceived == VFilterSelect.Select.LAST) {
+ getWidget().suggestionPopup.menu.selectLastItem();
+ } else {
+ getWidget().suggestionPopup.menu.selectFirstItem();
+ }
+
+ // This is used for paging so we update the keyboard selection
+ // variable as well.
+ MenuItem activeMenuItem = getWidget().suggestionPopup.menu
+ .getSelectedItem();
+ getWidget().suggestionPopup.menu
+ .setKeyboardSelectedItem(activeMenuItem);
+
+ // Update text field to contain the correct text
+ getWidget().setTextboxText(activeMenuItem.getText());
+ getWidget().tb.setSelectionRange(
+ getWidget().lastFilter.length(),
+ activeMenuItem.getText().length()
+ - getWidget().lastFilter.length());
+
+ getWidget().selectPopupItemWhenResponseIsReceived = VFilterSelect.Select.NONE; // reset
+ }
+ if (getWidget().updateSelectionWhenReponseIsReceived) {
+ getWidget().suggestionPopup.menu
+ .doPostFilterSelectedItemAction();
+ }
+ }
+
+ // Calculate minumum textarea width
+ getWidget().suggestionPopupMinWidth = getWidget().minWidth(captions);
+
+ getWidget().popupOpenerClicked = false;
+
+ if (!getWidget().initDone) {
+ getWidget().updateRootWidth();
+ }
+
+ // Focus dependent style names are lost during the update, so we add
+ // them here back again
+ if (getWidget().focused) {
+ getWidget().addStyleDependentName("focus");
+ }
+
+ getWidget().initDone = true;
+ }
+
+ @Override
+ public VFilterSelect getWidget() {
+ return (VFilterSelect) super.getWidget();
+ }
+
+ @Override
+ public void layout() {
+ VFilterSelect widget = getWidget();
+ if (widget.initDone) {
+ widget.updateRootWidth();
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java
new file mode 100644
index 0000000000..6e24a74e04
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/combobox/VFilterSelect.java
@@ -0,0 +1,1707 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.combobox;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.TextBox;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.shared.EventId;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.menubar.MenuBar;
+import com.vaadin.terminal.gwt.client.ui.menubar.MenuItem;
+
+/**
+ * Client side implementation of the Select component.
+ *
+ * TODO needs major refactoring (to be extensible etc)
+ */
+@SuppressWarnings("deprecation")
+public class VFilterSelect extends Composite implements Field, KeyDownHandler,
+ KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable,
+ SubPartAware {
+
+ /**
+ * Represents a suggestion in the suggestion popup box
+ */
+ public class FilterSelectSuggestion implements Suggestion, Command {
+
+ private final String key;
+ private final String caption;
+ private String iconUri;
+
+ /**
+ * Constructor
+ *
+ * @param uidl
+ * The UIDL recieved from the server
+ */
+ public FilterSelectSuggestion(UIDL uidl) {
+ key = uidl.getStringAttribute("key");
+ caption = uidl.getStringAttribute("caption");
+ if (uidl.hasAttribute("icon")) {
+ iconUri = client.translateVaadinUri(uidl
+ .getStringAttribute("icon"));
+ }
+ }
+
+ /**
+ * Gets the visible row in the popup as a HTML string. The string
+ * contains an image tag with the rows icon (if an icon has been
+ * specified) and the caption of the item
+ */
+
+ @Override
+ public String getDisplayString() {
+ final StringBuffer sb = new StringBuffer();
+ if (iconUri != null) {
+ sb.append("<img src=\"");
+ sb.append(Util.escapeAttribute(iconUri));
+ sb.append("\" alt=\"\" class=\"v-icon\" />");
+ }
+ String content;
+ if ("".equals(caption)) {
+ // Ensure that empty options use the same height as other
+ // options and are not collapsed (#7506)
+ content = "&nbsp;";
+ } else {
+ content = Util.escapeHTML(caption);
+ }
+ sb.append("<span>" + content + "</span>");
+ return sb.toString();
+ }
+
+ /**
+ * Get a string that represents this item. This is used in the text box.
+ */
+
+ @Override
+ public String getReplacementString() {
+ return caption;
+ }
+
+ /**
+ * Get the option key which represents the item on the server side.
+ *
+ * @return The key of the item
+ */
+ public int getOptionKey() {
+ return Integer.parseInt(key);
+ }
+
+ /**
+ * Get the URI of the icon. Used when constructing the displayed option.
+ *
+ * @return
+ */
+ public String getIconUri() {
+ return iconUri;
+ }
+
+ /**
+ * Executes a selection of this item.
+ */
+
+ @Override
+ public void execute() {
+ onSuggestionSelected(this);
+ }
+ }
+
+ /**
+ * Represents the popup box with the selection options. Wraps a suggestion
+ * menu.
+ */
+ public class SuggestionPopup extends VOverlay implements PositionCallback,
+ CloseHandler<PopupPanel> {
+
+ private static final String Z_INDEX = "30000";
+
+ protected 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;
+
+ /**
+ * Default constructor
+ */
+ 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");
+ DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
+ addCloseHandler(this);
+ }
+
+ /**
+ * Shows the popup where the user can see the filtered options
+ *
+ * @param currentSuggestions
+ * The filtered suggestions
+ * @param currentPage
+ * The current page number
+ * @param totalSuggestions
+ * The total amount of suggestions
+ */
+ public void showSuggestions(
+ Collection<FilterSelectSuggestion> currentSuggestions,
+ int currentPage, int totalSuggestions) {
+
+ // Add TT anchor point
+ DOM.setElementProperty(getElement(), "id",
+ "VAADIN_COMBOBOX_OPTIONLIST");
+
+ menu.setSuggestions(currentSuggestions);
+ final int x = VFilterSelect.this.getAbsoluteLeft();
+ topPosition = tb.getAbsoluteTop();
+ topPosition += tb.getOffsetHeight();
+ setPopupPosition(x, topPosition);
+
+ int nullOffset = (nullSelectionAllowed && "".equals(lastFilter) ? 1
+ : 0);
+ boolean firstPage = (currentPage == 0);
+ final int first = currentPage * pageLength + 1
+ - (firstPage ? 0 : nullOffset);
+ final int last = first + currentSuggestions.size() - 1
+ - (firstPage && "".equals(lastFilter) ? nullOffset : 0);
+ final int matches = totalSuggestions - nullOffset;
+ if (last > 0) {
+ // nullsel not counted, as requested by user
+ DOM.setInnerText(status, (matches == 0 ? 0 : first) + "-"
+ + last + "/" + matches);
+ } else {
+ DOM.setInnerText(status, "");
+ }
+ // We don't need to show arrows or statusbar if there is only one
+ // page
+ if (totalSuggestions <= pageLength || pageLength == 0) {
+ 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);
+
+ }
+
+ /**
+ * Should the next page button be visible to the user?
+ *
+ * @param active
+ */
+ private void setNextButtonActive(boolean active) {
+ if (active) {
+ DOM.sinkEvents(down, Event.ONCLICK);
+ DOM.setElementProperty(down, "className", CLASSNAME
+ + "-nextpage");
+ } else {
+ DOM.sinkEvents(down, 0);
+ DOM.setElementProperty(down, "className", CLASSNAME
+ + "-nextpage-off");
+ }
+ }
+
+ /**
+ * Should the previous page button be visible to the user
+ *
+ * @param active
+ */
+ private void setPrevButtonActive(boolean active) {
+ if (active) {
+ DOM.sinkEvents(up, Event.ONCLICK);
+ DOM.setElementProperty(up, "className", CLASSNAME + "-prevpage");
+ } else {
+ DOM.sinkEvents(up, 0);
+ DOM.setElementProperty(up, "className", CLASSNAME
+ + "-prevpage-off");
+ }
+
+ }
+
+ /**
+ * Selects the next item in the filtered selections
+ */
+ public void selectNextItem() {
+ final MenuItem cur = menu.getSelectedItem();
+ final int index = 1 + menu.getItems().indexOf(cur);
+ if (menu.getItems().size() > index) {
+ final MenuItem newSelectedItem = menu.getItems().get(index);
+ menu.selectItem(newSelectedItem);
+ tb.setText(newSelectedItem.getText());
+ tb.setSelectionRange(lastFilter.length(), newSelectedItem
+ .getText().length() - lastFilter.length());
+
+ } else if (hasNextPage()) {
+ selectPopupItemWhenResponseIsReceived = Select.FIRST;
+ filterOptions(currentPage + 1, lastFilter);
+ }
+ }
+
+ /**
+ * Selects the previous item in the filtered selections
+ */
+ public void selectPrevItem() {
+ final MenuItem cur = menu.getSelectedItem();
+ final int index = -1 + menu.getItems().indexOf(cur);
+ if (index > -1) {
+ final MenuItem newSelectedItem = 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) {
+ selectPopupItemWhenResponseIsReceived = Select.LAST;
+ filterOptions(currentPage - 1, lastFilter);
+ }
+ } else {
+ final MenuItem newSelectedItem = menu.getItems().get(
+ menu.getItems().size() - 1);
+ menu.selectItem(newSelectedItem);
+ tb.setText(newSelectedItem.getText());
+ tb.setSelectionRange(lastFilter.length(), newSelectedItem
+ .getText().length() - lastFilter.length());
+ }
+ }
+
+ /*
+ * Using a timer to scroll up or down the pages so when we receive lots
+ * of consecutive mouse wheel events the pages does not flicker.
+ */
+ private LazyPageScroller lazyPageScroller = new LazyPageScroller();
+
+ private class LazyPageScroller extends Timer {
+ private int pagesToScroll = 0;
+
+ @Override
+ public void run() {
+ if (pagesToScroll != 0) {
+ if (!waitingForFilteringResponse) {
+ /*
+ * Avoid scrolling while we are waiting for a response
+ * because otherwise the waiting flag will be reset in
+ * the first response and the second response will be
+ * ignored, causing an empty popup...
+ *
+ * As long as the scrolling delay is suitable
+ * double/triple clicks will work by scrolling two or
+ * three pages at a time and this should not be a
+ * problem.
+ */
+ filterOptions(currentPage + pagesToScroll, lastFilter);
+ }
+ pagesToScroll = 0;
+ }
+ }
+
+ public void scrollUp() {
+ if (currentPage + pagesToScroll > 0) {
+ pagesToScroll--;
+ cancel();
+ schedule(200);
+ }
+ }
+
+ public void scrollDown() {
+ if (totalMatches > (currentPage + pagesToScroll + 1)
+ * pageLength) {
+ pagesToScroll++;
+ cancel();
+ schedule(200);
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (event.getTypeInt() == Event.ONCLICK) {
+ final Element target = DOM.eventGetTarget(event);
+ if (target == up || target == DOM.getChild(up, 0)) {
+ lazyPageScroller.scrollUp();
+ } else if (target == down || target == DOM.getChild(down, 0)) {
+ lazyPageScroller.scrollDown();
+ }
+ } else if (event.getTypeInt() == Event.ONMOUSEWHEEL) {
+ int velocity = event.getMouseWheelVelocityY();
+ if (velocity > 0) {
+ lazyPageScroller.scrollDown();
+ } else {
+ lazyPageScroller.scrollUp();
+ }
+ }
+
+ /*
+ * Prevent the keyboard focus from leaving the textfield by
+ * preventing the default behaviour of the browser. Fixes #4285.
+ */
+ handleMouseDownEvent(event);
+ }
+
+ /**
+ * Should paging be enabled. If paging is enabled then only a certain
+ * amount of items are visible at a time and a scrollbar or buttons are
+ * visible to change page. If paging is turned of then all options are
+ * rendered into the popup menu.
+ *
+ * @param paging
+ * Should the paging be turned on?
+ */
+ 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)
+ */
+
+ @Override
+ 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(pageLength);
+ }
+ 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
+ - VFilterSelect.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 = VFilterSelect.this.getAbsoluteLeft()
+ + VFilterSelect.this.getOffsetWidth()
+ + Window.getScrollLeft() - offsetWidth;
+ if (left < 0) {
+ left = 0;
+ }
+ } else {
+ left = getPopupLeft();
+ }
+ setPopupPosition(left, top);
+ }
+
+ /**
+ * Was the popup just closed?
+ *
+ * @return true if popup was just closed
+ */
+ public boolean isJustClosed() {
+ final long now = (new Date()).getTime();
+ return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google
+ * .gwt.event.logical.shared.CloseEvent)
+ */
+
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (event.isAutoClosed()) {
+ lastAutoClosed = (new Date()).getTime();
+ }
+ }
+
+ /**
+ * Updates style names in suggestion popup to help theme building.
+ *
+ * @param uidl
+ * UIDL for the whole combo box
+ * @param componentState
+ * shared state of the combo box
+ */
+ public void updateStyleNames(UIDL uidl, ComponentState componentState) {
+ setStyleName(CLASSNAME + "-suggestpopup");
+ if (componentState.hasStyles()) {
+ for (String style : componentState.getStyles()) {
+ if (!"".equals(style)) {
+ addStyleDependentName(style);
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * The menu where the suggestions are rendered
+ */
+ public class SuggestionMenu extends MenuBar implements SubPartAware,
+ LoadHandler {
+
+ /**
+ * Tracks the item that is currently selected using the keyboard. This
+ * is need only because mouseover changes the selection and we do not
+ * want to use that selection when pressing enter to select the item.
+ */
+ private MenuItem keyboardSelectedItem;
+
+ private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(
+ 100, new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (suggestionPopup.isVisible()
+ && suggestionPopup.isAttached()) {
+ setWidth("");
+ DOM.setStyleAttribute(
+ DOM.getFirstChild(getElement()), "width",
+ "");
+ suggestionPopup
+ .setPopupPositionAndShow(suggestionPopup);
+ }
+
+ }
+ });
+
+ /**
+ * Default constructor
+ */
+ SuggestionMenu() {
+ super(true);
+ setStyleName(CLASSNAME + "-suggestmenu");
+ addDomHandler(this, LoadEvent.getType());
+ }
+
+ /**
+ * 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");
+ }
+ }
+
+ /**
+ * Sets the suggestions rendered in the menu
+ *
+ * @param suggestions
+ * The suggestions to be rendered in the menu
+ */
+ public void setSuggestions(
+ Collection<FilterSelectSuggestion> suggestions) {
+ // Reset keyboard selection when contents is updated to avoid
+ // reusing old, invalid data
+ setKeyboardSelectedItem(null);
+
+ clearItems();
+ final Iterator<FilterSelectSuggestion> it = suggestions.iterator();
+ while (it.hasNext()) {
+ final FilterSelectSuggestion s = it.next();
+ final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
+
+ Util.sinkOnloadForImages(mi.getElement());
+
+ this.addItem(mi);
+ if (s == currentSuggestion) {
+ selectItem(mi);
+ }
+ }
+ }
+
+ /**
+ * Send the current selection to the server. Triggered when a selection
+ * is made or on a blur event.
+ */
+ public void doSelectedItemAction() {
+ // do not send a value change event if null was and stays selected
+ final String enteredItemValue = tb.getText();
+ if (nullSelectionAllowed && "".equals(enteredItemValue)
+ && selectedOptionKey != null
+ && !"".equals(selectedOptionKey)) {
+ if (nullSelectItem) {
+ reset();
+ return;
+ }
+ // null is not visible on pages != 0, and not visible when
+ // filtering: handle separately
+ client.updateVariable(paintableId, "filter", "", false);
+ client.updateVariable(paintableId, "page", 0, false);
+ client.updateVariable(paintableId, "selected", new String[] {},
+ immediate);
+ suggestionPopup.hide();
+ return;
+ }
+
+ updateSelectionWhenReponseIsReceived = waitingForFilteringResponse;
+ if (!waitingForFilteringResponse) {
+ doPostFilterSelectedItemAction();
+ }
+ }
+
+ /**
+ * Triggered after a selection has been made
+ */
+ public void doPostFilterSelectedItemAction() {
+ final MenuItem item = getSelectedItem();
+ final String enteredItemValue = tb.getText();
+
+ updateSelectionWhenReponseIsReceived = false;
+
+ // check for exact match in menu
+ int p = getItems().size();
+ if (p > 0) {
+ for (int i = 0; i < p; i++) {
+ final MenuItem potentialExactMatch = getItems().get(i);
+ if (potentialExactMatch.getText().equals(enteredItemValue)) {
+ selectItem(potentialExactMatch);
+ // do not send a value change event if null was and
+ // stays selected
+ if (!"".equals(enteredItemValue)
+ || (selectedOptionKey != null && !""
+ .equals(selectedOptionKey))) {
+ 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)
+ && (filteringmode == FILTERINGMODE_CONTAINS ? item
+ .getText().toLowerCase()
+ .contains(lastFilter.toLowerCase()) : item
+ .getText().toLowerCase()
+ .startsWith(lastFilter.toLowerCase()))) {
+ doItemAction(item, true);
+ } else {
+ // currentSuggestion has key="" for nullselection
+ if (currentSuggestion != null
+ && !currentSuggestion.key.equals("")) {
+ // An item (not null) selected
+ String text = currentSuggestion.getReplacementString();
+ tb.setText(text);
+ selectedOptionKey = currentSuggestion.key;
+ } else {
+ // Null selected
+ tb.setText("");
+ selectedOptionKey = null;
+ }
+ }
+ suggestionPopup.hide();
+ }
+
+ private static final String SUBPART_PREFIX = "item";
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ int index = Integer.parseInt(subPart.substring(SUBPART_PREFIX
+ .length()));
+
+ MenuItem item = getItems().get(index);
+
+ return item.getElement();
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ Element menuItemRoot = subElement;
+ while (menuItemRoot != null
+ && !menuItemRoot.getTagName().equalsIgnoreCase("td")) {
+ menuItemRoot = menuItemRoot.getParentElement().cast();
+ }
+ // "menuItemRoot" is now the root of the menu item
+
+ final int itemCount = getItems().size();
+ for (int i = 0; i < itemCount; i++) {
+ if (getItems().get(i).getElement() == menuItemRoot) {
+ String name = SUBPART_PREFIX + i;
+ return name;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoad(LoadEvent event) {
+ // Handle icon onload events to ensure shadow is resized
+ // correctly
+ delayedImageLoadExecutioner.trigger();
+
+ }
+
+ public void selectFirstItem() {
+ MenuItem firstItem = getItems().get(0);
+ selectItem(firstItem);
+ }
+
+ private MenuItem getKeyboardSelectedItem() {
+ return keyboardSelectedItem;
+ }
+
+ protected void setKeyboardSelectedItem(MenuItem firstItem) {
+ keyboardSelectedItem = firstItem;
+ }
+
+ public void selectLastItem() {
+ List<MenuItem> items = getItems();
+ MenuItem lastItem = items.get(items.size() - 1);
+ selectItem(lastItem);
+ }
+ }
+
+ 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 = "v-filterselect";
+ private static final String STYLE_NO_INPUT = "no-input";
+
+ protected int pageLength = 10;
+
+ private boolean enableDebug = false;
+
+ private final FlowPanel panel = new FlowPanel();
+
+ /**
+ * The text box where the filter is written
+ */
+ protected final TextBox tb = new TextBox() {
+
+ // Overridden to avoid selecting text when text input is disabled
+ @Override
+ public void setSelectionRange(int pos, int length) {
+ if (textInputEnabled) {
+ super.setSelectionRange(pos, length);
+ } else {
+ super.setSelectionRange(getValue().length(), 0);
+ }
+ };
+ };
+
+ protected final SuggestionPopup suggestionPopup = new SuggestionPopup();
+
+ /**
+ * Used when measuring the width of the popup
+ */
+ private final HTML popupOpener = new HTML("") {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ /*
+ * Prevent the keyboard focus from leaving the textfield by
+ * preventing the default behaviour of the browser. Fixes #4285.
+ */
+ handleMouseDownEvent(event);
+ }
+ };
+
+ private final Image selectedItemIcon = new Image();
+
+ protected ApplicationConnection client;
+
+ protected String paintableId;
+
+ protected int currentPage;
+
+ /**
+ * A collection of available suggestions (options) as received from the
+ * server.
+ */
+ protected final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>();
+
+ protected boolean immediate;
+
+ protected String selectedOptionKey;
+
+ protected boolean waitingForFilteringResponse = false;
+ protected boolean updateSelectionWhenReponseIsReceived = false;
+ private boolean tabPressedWhenPopupOpen = false;
+ protected boolean initDone = false;
+
+ protected String lastFilter = "";
+
+ protected enum Select {
+ NONE, FIRST, LAST
+ };
+
+ protected Select selectPopupItemWhenResponseIsReceived = Select.NONE;
+
+ /**
+ * The current suggestion selected from the dropdown. This is one of the
+ * values in currentSuggestions except when filtering, in this case
+ * currentSuggestion might not be in currentSuggestions.
+ */
+ protected FilterSelectSuggestion currentSuggestion;
+
+ protected int totalMatches;
+ protected boolean allowNewItem;
+ protected boolean nullSelectionAllowed;
+ protected boolean nullSelectItem;
+ protected boolean enabled;
+ protected boolean readonly;
+
+ protected int filteringmode = FILTERINGMODE_OFF;
+
+ // shown in unfocused empty field, disappears on focus (e.g "Search here")
+ private static final String CLASSNAME_PROMPT = "prompt";
+ protected static final String ATTR_INPUTPROMPT = "prompt";
+ public static final String ATTR_NO_TEXT_INPUT = "noInput";
+ protected String inputPrompt = "";
+ protected 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
+ protected boolean popupOpenerClicked;
+ protected int suggestionPopupMinWidth = 0;
+ private int popupWidth = -1;
+ /*
+ * Stores the last new item string to avoid double submissions. Cleared on
+ * uidl updates
+ */
+ protected String lastNewItemString;
+ protected boolean focused = false;
+
+ /**
+ * If set to false, the component should not allow entering text to the
+ * field even for filtering.
+ */
+ private boolean textInputEnabled = true;
+
+ /**
+ * Default constructor
+ */
+ public VFilterSelect() {
+ selectedItemIcon.setStyleName("v-icon");
+ selectedItemIcon.addLoadHandler(new LoadHandler() {
+
+ @Override
+ public void onLoad(LoadEvent event) {
+ if (BrowserInfo.get().isIE8()) {
+ // IE8 needs some help to discover it should reposition the
+ // text field
+ forceReflow();
+ }
+ updateRootWidth();
+ updateSelectedIconPosition();
+ }
+ });
+
+ popupOpener.sinkEvents(Event.ONMOUSEDOWN);
+ panel.add(tb);
+ panel.add(popupOpener);
+ initWidget(panel);
+ setStyleName(CLASSNAME);
+ tb.addKeyDownHandler(this);
+ tb.addKeyUpHandler(this);
+ tb.setStyleName(CLASSNAME + "-input");
+ tb.addFocusHandler(this);
+ tb.addBlurHandler(this);
+ tb.addClickHandler(this);
+ popupOpener.setStyleName(CLASSNAME + "-button");
+ popupOpener.addClickHandler(this);
+ }
+
+ /**
+ * Does the Select have more pages?
+ *
+ * @return true if a next page exists, else false if the current page is the
+ * last page
+ */
+ public boolean hasNextPage() {
+ if (totalMatches > (currentPage + 1) * pageLength) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Filters the options at a certain page. Uses the text box input as a
+ * filter
+ *
+ * @param page
+ * The page which items are to be filtered
+ */
+ public void filterOptions(int page) {
+ filterOptions(page, tb.getText());
+ }
+
+ /**
+ * Filters the options at certain page using the given filter
+ *
+ * @param page
+ * The page to filter
+ * @param filter
+ * The filter to apply to the components
+ */
+ public void filterOptions(int page, String filter) {
+ filterOptions(page, filter, true);
+ }
+
+ /**
+ * Filters the options at certain page using the given filter
+ *
+ * @param page
+ * The page to filter
+ * @param filter
+ * The filter to apply to the options
+ * @param immediate
+ * Whether to send the options request immediately
+ */
+ private void filterOptions(int page, String filter, boolean immediate) {
+ 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;
+ }
+ }
+
+ waitingForFilteringResponse = true;
+ client.updateVariable(paintableId, "filter", filter, false);
+ client.updateVariable(paintableId, "page", page, immediate);
+ lastFilter = filter;
+ currentPage = page;
+ }
+
+ protected void updateReadOnly() {
+ tb.setReadOnly(readonly || !textInputEnabled);
+ }
+
+ protected void setTextInputEnabled(boolean textInputEnabled) {
+ // Always update styles as they might have been overwritten
+ if (textInputEnabled) {
+ removeStyleDependentName(STYLE_NO_INPUT);
+ } else {
+ addStyleDependentName(STYLE_NO_INPUT);
+ }
+
+ if (this.textInputEnabled == textInputEnabled) {
+ return;
+ }
+
+ this.textInputEnabled = textInputEnabled;
+ updateReadOnly();
+ }
+
+ /**
+ * Sets the text in the text box.
+ *
+ * @param text
+ * the text to set in the text box
+ */
+ protected void setTextboxText(final String text) {
+ tb.setText(text);
+ }
+
+ /**
+ * Turns prompting on. When prompting is turned on a command prompt is shown
+ * in the text box if nothing has been entered.
+ */
+ protected void setPromptingOn() {
+ if (!prompting) {
+ prompting = true;
+ addStyleDependentName(CLASSNAME_PROMPT);
+ }
+ setTextboxText(inputPrompt);
+ }
+
+ /**
+ * Turns prompting off. When prompting is turned on a command prompt is
+ * shown in the text box if nothing has been entered.
+ *
+ * @param text
+ * The text the text box should contain.
+ */
+ protected void setPromptingOff(String text) {
+ setTextboxText(text);
+ if (prompting) {
+ prompting = false;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ }
+
+ /**
+ * Triggered when a suggestion is selected
+ *
+ * @param suggestion
+ * The suggestion that just got selected.
+ */
+ public void onSuggestionSelected(FilterSelectSuggestion suggestion) {
+ updateSelectionWhenReponseIsReceived = false;
+
+ 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) || ("".equals(newKey) && selectedOptionKey == null))) {
+ selectedOptionKey = newKey;
+ client.updateVariable(paintableId, "selected",
+ new String[] { selectedOptionKey }, immediate);
+ // currentPage = -1; // forget the page
+ }
+ suggestionPopup.hide();
+ }
+
+ /**
+ * Sets the icon URI of the selected item. The icon is shown on the left
+ * side of the item caption text. Set the URI to null to remove the icon.
+ *
+ * @param iconUri
+ * The URI of the icon
+ */
+ protected void setSelectedItemIcon(String iconUri) {
+ if (iconUri == null || iconUri.length() == 0) {
+ if (selectedItemIcon.isAttached()) {
+ panel.remove(selectedItemIcon);
+ if (BrowserInfo.get().isIE8()) {
+ // IE8 needs some help to discover it should reposition the
+ // text field
+ forceReflow();
+ }
+ updateRootWidth();
+ }
+ } else {
+ panel.insert(selectedItemIcon, 0);
+ selectedItemIcon.setUrl(iconUri);
+ updateRootWidth();
+ updateSelectedIconPosition();
+ }
+ }
+
+ private void forceReflow() {
+ Util.setStyleTemporarily(tb.getElement(), "zoom", "1");
+ }
+
+ /**
+ * Positions the icon vertically in the middle. Should be called after the
+ * icon has loaded
+ */
+ private void updateSelectedIconPosition() {
+ // Position icon vertically to middle
+ int availableHeight = 0;
+ availableHeight = getOffsetHeight();
+
+ int iconHeight = Util.getRequiredHeight(selectedItemIcon);
+ int marginTop = (availableHeight - iconHeight) / 2;
+ DOM.setStyleAttribute(selectedItemIcon.getElement(), "marginTop",
+ marginTop + "px");
+ }
+
+ private static Set<Integer> navigationKeyCodes = new HashSet<Integer>();
+ static {
+ navigationKeyCodes.add(KeyCodes.KEY_DOWN);
+ navigationKeyCodes.add(KeyCodes.KEY_UP);
+ navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN);
+ navigationKeyCodes.add(KeyCodes.KEY_PAGEUP);
+ navigationKeyCodes.add(KeyCodes.KEY_ENTER);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (enabled && !readonly) {
+ int keyCode = event.getNativeKeyCode();
+
+ debug("key down: " + keyCode);
+ if (waitingForFilteringResponse
+ && navigationKeyCodes.contains(keyCode)) {
+ /*
+ * Keyboard navigation events should not be handled while we are
+ * waiting for a response. This avoids flickering, disappearing
+ * items, wrongly interpreted responses and more.
+ */
+ debug("Ignoring " + keyCode
+ + " because we are waiting for a filtering response");
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ return;
+ }
+
+ if (suggestionPopup.isAttached()) {
+ debug("Keycode " + keyCode + " target is popup");
+ popupKeyDown(event);
+ } else {
+ debug("Keycode " + keyCode + " target is text field");
+ inputFieldKeyDown(event);
+ }
+ }
+ }
+
+ private void debug(String string) {
+ if (enableDebug) {
+ VConsole.error(string);
+ }
+ }
+
+ /**
+ * Triggered when a key is pressed in the text box
+ *
+ * @param event
+ * The KeyDownEvent
+ */
+ private void inputFieldKeyDown(KeyDownEvent event) {
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ // open popup as from gadget
+ filterOptions(-1, "");
+ lastFilter = "";
+ tb.selectAll();
+ break;
+ case KeyCodes.KEY_ENTER:
+ /*
+ * This only handles the case when new items is allowed, a text is
+ * entered, the popup opener button is clicked to close the popup
+ * and enter is then pressed (see #7560).
+ */
+ if (!allowNewItem) {
+ return;
+ }
+
+ if (currentSuggestion != null
+ && tb.getText().equals(
+ currentSuggestion.getReplacementString())) {
+ // Retain behavior from #6686 by returning without stopping
+ // propagation if there's nothing to do
+ return;
+ }
+ suggestionPopup.menu.doSelectedItemAction();
+
+ event.stopPropagation();
+ break;
+ }
+
+ }
+
+ /**
+ * Triggered when a key was pressed in the suggestion popup.
+ *
+ * @param event
+ * The KeyDownEvent of the key
+ */
+ private void popupKeyDown(KeyDownEvent event) {
+ // Propagation of handled events is stopped so other handlers such as
+ // shortcut key handlers do not also handle the same events.
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ suggestionPopup.selectNextItem();
+ suggestionPopup.menu.setKeyboardSelectedItem(suggestionPopup.menu
+ .getSelectedItem());
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_UP:
+ suggestionPopup.selectPrevItem();
+ suggestionPopup.menu.setKeyboardSelectedItem(suggestionPopup.menu
+ .getSelectedItem());
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_PAGEDOWN:
+ if (hasNextPage()) {
+ filterOptions(currentPage + 1, lastFilter);
+ }
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_PAGEUP:
+ if (currentPage > 0) {
+ filterOptions(currentPage - 1, lastFilter);
+ }
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_TAB:
+ tabPressedWhenPopupOpen = true;
+ filterOptions(currentPage);
+ // onBlur() takes care of the rest
+ break;
+ case KeyCodes.KEY_ESCAPE:
+ reset();
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_ENTER:
+ if (suggestionPopup.menu.getKeyboardSelectedItem() == null) {
+ /*
+ * Nothing selected using up/down. Happens e.g. when entering a
+ * text (causes popup to open) and then pressing enter.
+ */
+ if (!allowNewItem) {
+ /*
+ * New items are not allowed: If there is only one
+ * suggestion, select that. Otherwise do nothing.
+ */
+ if (currentSuggestions.size() == 1) {
+ onSuggestionSelected(currentSuggestions.get(0));
+ }
+ } else {
+ // Handle addition of new items.
+ suggestionPopup.menu.doSelectedItemAction();
+ }
+ } else {
+ /*
+ * Get the suggestion that was navigated to using up/down.
+ */
+ currentSuggestion = ((FilterSelectSuggestion) suggestionPopup.menu
+ .getKeyboardSelectedItem().getCommand());
+ onSuggestionSelected(currentSuggestion);
+ }
+
+ event.stopPropagation();
+ break;
+ }
+
+ }
+
+ /**
+ * Triggered when a key was depressed
+ *
+ * @param event
+ * The KeyUpEvent of the key depressed
+ */
+
+ @Override
+ public void onKeyUp(KeyUpEvent event) {
+ if (enabled && !readonly) {
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_ENTER:
+ case KeyCodes.KEY_TAB:
+ case KeyCodes.KEY_SHIFT:
+ case KeyCodes.KEY_CTRL:
+ case KeyCodes.KEY_ALT:
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ case KeyCodes.KEY_ESCAPE:
+ ; // NOP
+ break;
+ default:
+ if (textInputEnabled) {
+ filterOptions(currentPage);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Resets the Select to its initial state
+ */
+ private void reset() {
+ if (currentSuggestion != null) {
+ String text = currentSuggestion.getReplacementString();
+ setPromptingOff(text);
+ selectedOptionKey = currentSuggestion.key;
+ } else {
+ if (focused) {
+ setPromptingOff("");
+ } else {
+ setPromptingOn();
+ }
+ selectedOptionKey = null;
+ }
+ lastFilter = "";
+ suggestionPopup.hide();
+ }
+
+ /**
+ * Listener for popupopener
+ */
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (textInputEnabled
+ && event.getNativeEvent().getEventTarget().cast() == tb
+ .getElement()) {
+ // Don't process clicks on the text field if text input is enabled
+ return;
+ }
+ if (enabled && !readonly) {
+ // ask suggestionPopup if it was just closed, we are using GWT
+ // Popup's auto close feature
+ if (!suggestionPopup.isJustClosed()) {
+ // If a focus event is not going to be sent, send the options
+ // request immediately; otherwise queue in the same burst as the
+ // focus event. Fixes #8321.
+ boolean immediate = focused
+ || !client.hasEventListeners(this, EventId.FOCUS);
+ filterOptions(-1, "", immediate);
+ popupOpenerClicked = true;
+ lastFilter = "";
+ }
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ focus();
+ tb.selectAll();
+ }
+ }
+
+ /**
+ * Calculate minimum width for FilterSelect textarea
+ */
+ protected 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;
+ }-*/;
+
+ /**
+ * A flag which prevents a focus event from taking place
+ */
+ boolean iePreventNextFocus = false;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+
+ @Override
+ public void onFocus(FocusEvent event) {
+
+ /*
+ * When we disable a blur event in ie we need to refocus the textfield.
+ * This will cause a focus event we do not want to process, so in that
+ * case we just ignore it.
+ */
+ if (BrowserInfo.get().isIE() && iePreventNextFocus) {
+ iePreventNextFocus = false;
+ return;
+ }
+
+ focused = true;
+ if (prompting && !readonly) {
+ setPromptingOff("");
+ }
+ addStyleDependentName("focus");
+
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(paintableId, EventId.FOCUS, "", true);
+ }
+ }
+
+ /**
+ * A flag which cancels the blur event and sets the focus back to the
+ * textfield if the Browser is IE
+ */
+ boolean preventNextBlurEventInIE = false;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+
+ @Override
+ public void onBlur(BlurEvent event) {
+
+ if (BrowserInfo.get().isIE() && preventNextBlurEventInIE) {
+ /*
+ * Clicking in the suggestion popup or on the popup button in IE
+ * causes a blur event to be sent for the field. In other browsers
+ * this is prevented by canceling/preventing default behavior for
+ * the focus event, in IE we handle it here by refocusing the text
+ * field and ignoring the resulting focus event for the textfield
+ * (in onFocus).
+ */
+ preventNextBlurEventInIE = false;
+
+ Element focusedElement = Util.getIEFocusedElement();
+ if (getElement().isOrHasChild(focusedElement)
+ || suggestionPopup.getElement()
+ .isOrHasChild(focusedElement)) {
+
+ // IF the suggestion popup or another part of the VFilterSelect
+ // was focused, move the focus back to the textfield and prevent
+ // the triggered focus event (in onFocus).
+ iePreventNextFocus = true;
+ tb.setFocus(true);
+ return;
+ }
+ }
+
+ focused = false;
+ if (!readonly) {
+ // much of the TAB handling takes place here
+ if (tabPressedWhenPopupOpen) {
+ tabPressedWhenPopupOpen = false;
+ suggestionPopup.menu.doSelectedItemAction();
+ suggestionPopup.hide();
+ } else if (!suggestionPopup.isAttached()
+ || suggestionPopup.isJustClosed()) {
+ suggestionPopup.menu.doSelectedItemAction();
+ }
+ if (selectedOptionKey == null) {
+ setPromptingOn();
+ } else if (currentSuggestion != null) {
+ setPromptingOff(currentSuggestion.caption);
+ }
+ }
+ removeStyleDependentName("focus");
+
+ if (client.hasEventListeners(this, EventId.BLUR)) {
+ client.updateVariable(paintableId, EventId.BLUR, "", true);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Focusable#focus()
+ */
+
+ @Override
+ public void focus() {
+ focused = true;
+ if (prompting && !readonly) {
+ setPromptingOff("");
+ }
+ tb.setFocus(true);
+ }
+
+ /**
+ * Calculates the width of the select if the select has undefined width.
+ * Should be called when the width changes or when the icon changes.
+ */
+ protected void updateRootWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ if (paintable.isUndefinedWidth()) {
+
+ /*
+ * When the select has a undefined with we need to check that we are
+ * only setting the text box width relative to the first page width
+ * of the items. If this is not done the text box width will change
+ * when the popup is used to view longer items than the text box is
+ * wide.
+ */
+ int w = Util.getRequiredWidth(this);
+ if ((!initDone || currentPage + 1 < 0)
+ && suggestionPopupMinWidth > w) {
+ /*
+ * We want to compensate for the paddings just to preserve the
+ * exact size as in Vaadin 6.x, but we get here before
+ * MeasuredSize has been initialized.
+ * Util.measureHorizontalPaddingAndBorder does not work with
+ * border-box, so we must do this the hard way.
+ */
+ Style style = getElement().getStyle();
+ String originalPadding = style.getPadding();
+ String originalBorder = style.getBorderWidth();
+ style.setPaddingLeft(0, Unit.PX);
+ style.setBorderWidth(0, Unit.PX);
+ int offset = w - Util.getRequiredWidth(this);
+ style.setProperty("padding", originalPadding);
+ style.setProperty("borderWidth", originalBorder);
+
+ setWidth(suggestionPopupMinWidth + offset + "px");
+ }
+
+ /*
+ * Lock the textbox width to its current value if it's not already
+ * locked
+ */
+ if (!tb.getElement().getStyle().getWidth().endsWith("px")) {
+ tb.setWidth((tb.getOffsetWidth() - selectedItemIcon
+ .getOffsetWidth()) + "px");
+ }
+ }
+ }
+
+ /**
+ * Get the width of the select in pixels where the text area and icon has
+ * been included.
+ *
+ * @return The width in pixels
+ */
+ private int getMainWidth() {
+ return getOffsetWidth();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (width.length() != 0) {
+ tb.setWidth("100%");
+ }
+ }
+
+ /**
+ * Handles special behavior of the mouse down event
+ *
+ * @param event
+ */
+ private void handleMouseDownEvent(Event event) {
+ /*
+ * Prevent the keyboard focus from leaving the textfield by preventing
+ * the default behaviour of the browser. Fixes #4285.
+ */
+ if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ /*
+ * In IE the above wont work, the blur event will still trigger. So,
+ * we set a flag here to prevent the next blur event from happening.
+ * This is not needed if do not already have focus, in that case
+ * there will not be any blur event and we should not cancel the
+ * next blur.
+ */
+ if (BrowserInfo.get().isIE() && focused) {
+ preventNextBlurEventInIE = true;
+ }
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ suggestionPopup.hide();
+ }
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if ("textbox".equals(subPart)) {
+ return this.tb.getElement();
+ } else if ("button".equals(subPart)) {
+ return this.popupOpener.getElement();
+ }
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (tb.getElement().isOrHasChild(subElement)) {
+ return "textbox";
+ } else if (popupOpener.getElement().isOrHasChild(subElement)) {
+ return "button";
+ }
+ return null;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java
new file mode 100644
index 0000000000..7d07172cd1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/CssLayoutConnector.java
@@ -0,0 +1,166 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.csslayout;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.LayoutClickRpc;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.shared.ui.csslayout.CssLayoutServerRpc;
+import com.vaadin.shared.ui.csslayout.CssLayoutState;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.csslayout.VCssLayout.FlowPane;
+import com.vaadin.ui.CssLayout;
+
+@Connect(CssLayout.class)
+public class CssLayoutConnector extends AbstractLayoutConnector {
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return Util.getConnectorForElement(getConnection(), getWidget(),
+ element);
+ }
+
+ @Override
+ protected LayoutClickRpc getLayoutClickRPC() {
+ return rpc;
+ };
+ };
+
+ private CssLayoutServerRpc rpc;
+
+ private Map<ComponentConnector, VCaption> childToCaption = new HashMap<ComponentConnector, VCaption>();
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(CssLayoutServerRpc.class, this);
+ }
+
+ @Override
+ public CssLayoutState getState() {
+ return (CssLayoutState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ getWidget().setMarginStyles(
+ new VMarginInfo(getState().getMarginsBitmask()));
+
+ for (ComponentConnector child : getChildComponents()) {
+ if (!getState().getChildCss().containsKey(child)) {
+ continue;
+ }
+ String css = getState().getChildCss().get(child);
+ Style style = child.getWidget().getElement().getStyle();
+ // should we remove styles also? How can we know what we have added
+ // as it is added directly to the child component?
+ String[] cssRules = css.split(";");
+ for (String cssRule : cssRules) {
+ String parts[] = cssRule.split(":");
+ if (parts.length == 2) {
+ style.setProperty(makeCamelCase(parts[0].trim()),
+ parts[1].trim());
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ int index = 0;
+ FlowPane cssLayoutWidgetContainer = getWidget().panel;
+ for (ComponentConnector child : getChildComponents()) {
+ VCaption childCaption = childToCaption.get(child);
+ if (childCaption != null) {
+ cssLayoutWidgetContainer.addOrMove(childCaption, index++);
+ }
+ cssLayoutWidgetContainer.addOrMove(child.getWidget(), index++);
+ }
+
+ // Detach old child widgets and possibly their caption
+ for (ComponentConnector child : event.getOldChildren()) {
+ if (child.getParent() == this) {
+ // Skip current children
+ continue;
+ }
+ cssLayoutWidgetContainer.remove(child.getWidget());
+ VCaption vCaption = childToCaption.remove(child);
+ if (vCaption != null) {
+ cssLayoutWidgetContainer.remove(vCaption);
+ }
+ }
+ }
+
+ private static final String makeCamelCase(String cssProperty) {
+ // TODO this might be cleaner to implement with regexp
+ while (cssProperty.contains("-")) {
+ int indexOf = cssProperty.indexOf("-");
+ cssProperty = cssProperty.substring(0, indexOf)
+ + String.valueOf(cssProperty.charAt(indexOf + 1))
+ .toUpperCase() + cssProperty.substring(indexOf + 2);
+ }
+ if ("float".equals(cssProperty)) {
+ if (BrowserInfo.get().isIE()) {
+ return "styleFloat";
+ } else {
+ return "cssFloat";
+ }
+ }
+ return cssProperty;
+ }
+
+ @Override
+ public VCssLayout getWidget() {
+ return (VCssLayout) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector child) {
+ Widget childWidget = child.getWidget();
+ FlowPane cssLayoutWidgetContainer = getWidget().panel;
+ int widgetPosition = cssLayoutWidgetContainer
+ .getWidgetIndex(childWidget);
+
+ VCaption caption = childToCaption.get(child);
+ if (VCaption.isNeeded(child.getState())) {
+ if (caption == null) {
+ caption = new VCaption(child, getConnection());
+ childToCaption.put(child, caption);
+ }
+ if (!caption.isAttached()) {
+ // Insert caption at widget index == before widget
+ cssLayoutWidgetContainer.insert(caption, widgetPosition);
+ }
+ caption.updateCaption();
+ } else if (caption != null) {
+ childToCaption.remove(child);
+ cssLayoutWidgetContainer.remove(caption);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java
new file mode 100644
index 0000000000..53f8ca6c8a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/csslayout/VCssLayout.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.csslayout;
+
+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.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.StyleConstants;
+
+public class VCssLayout extends SimplePanel {
+ public static final String TAGNAME = "csslayout";
+ public static final String CLASSNAME = "v-" + TAGNAME;
+
+ FlowPane panel = new FlowPane();
+
+ Element margin = DOM.createDiv();
+
+ public VCssLayout() {
+ super();
+ getElement().appendChild(margin);
+ setStyleName(CLASSNAME);
+ margin.setClassName(CLASSNAME + "-margin");
+ setWidget(panel);
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ return margin;
+ }
+
+ public class FlowPane extends FlowPanel {
+
+ public FlowPane() {
+ super();
+ setStyleName(CLASSNAME + "-container");
+ }
+
+ void addOrMove(Widget child, int index) {
+ if (child.getParent() == this) {
+ int currentIndex = getWidgetIndex(child);
+ if (index == currentIndex) {
+ return;
+ }
+ }
+ insert(child, index);
+ }
+
+ }
+
+ /**
+ * Sets CSS classes for margin based on the given parameters.
+ *
+ * @param margins
+ * A {@link VMarginInfo} object that provides info on
+ * top/left/bottom/right margins
+ */
+ protected void setMarginStyles(VMarginInfo margins) {
+ setStyleName(margin, VCssLayout.CLASSNAME + "-"
+ + StyleConstants.MARGIN_TOP, margins.hasTop());
+ setStyleName(margin, VCssLayout.CLASSNAME + "-"
+ + StyleConstants.MARGIN_RIGHT, margins.hasRight());
+ setStyleName(margin, VCssLayout.CLASSNAME + "-"
+ + StyleConstants.MARGIN_BOTTOM, margins.hasBottom());
+ setStyleName(margin, VCssLayout.CLASSNAME + "-"
+ + StyleConstants.MARGIN_LEFT, margins.hasLeft());
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java
new file mode 100644
index 0000000000..f7740a9205
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/CustomComponentConnector.java
@@ -0,0 +1,40 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.customcomponent;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.ui.CustomComponent;
+
+@Connect(value = CustomComponent.class, loadStyle = LoadStyle.EAGER)
+public class CustomComponentConnector extends
+ AbstractComponentContainerConnector {
+
+ @Override
+ public VCustomComponent getWidget() {
+ return (VCustomComponent) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP, custom component dont render composition roots caption
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ ComponentConnector newChild = null;
+ if (getChildComponents().size() == 1) {
+ newChild = getChildComponents().get(0);
+ }
+
+ VCustomComponent customComponent = getWidget();
+ customComponent.setWidget(newChild.getWidget());
+
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java
new file mode 100644
index 0000000000..2b27bd0e58
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/customcomponent/VCustomComponent.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.customcomponent;
+
+import com.google.gwt.user.client.ui.SimplePanel;
+
+public class VCustomComponent extends SimplePanel {
+
+ private static final String CLASSNAME = "v-customcomponent";
+
+ public VCustomComponent() {
+ super();
+ setStyleName(CLASSNAME);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java
new file mode 100644
index 0000000000..b4f42c36aa
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/customfield/CustomFieldConnector.java
@@ -0,0 +1,22 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.customfield;
+
+import com.google.gwt.core.client.GWT;
+import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ui.customcomponent.CustomComponentConnector;
+import com.vaadin.ui.CustomField;
+
+@Connect(value = CustomField.class)
+public class CustomFieldConnector extends CustomComponentConnector {
+ @Override
+ protected SharedState createState() {
+ // Workaround as CustomFieldConnector does not extend
+ // AbstractFieldConnector.
+ return GWT.create(AbstractFieldState.class);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java
new file mode 100644
index 0000000000..9d973227d1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/CustomLayoutConnector.java
@@ -0,0 +1,124 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.customlayout;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.customlayout.CustomLayoutState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.ui.CustomLayout;
+
+@Connect(CustomLayout.class)
+public class CustomLayoutConnector extends AbstractLayoutConnector implements
+ SimpleManagedLayout, Paintable {
+
+ @Override
+ public CustomLayoutState getState() {
+ return (CustomLayoutState) super.getState();
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().client = getConnection();
+ getWidget().pid = getConnectorId();
+
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ // Evaluate scripts
+ VCustomLayout.eval(getWidget().scripts);
+ getWidget().scripts = null;
+
+ }
+
+ private void updateHtmlTemplate() {
+ if (getWidget().hasTemplate()) {
+ // We (currently) only do this once. You can't change the template
+ // later on.
+ return;
+ }
+ String templateName = getState().getTemplateName();
+ String templateContents = getState().getTemplateContents();
+
+ if (templateName != null) {
+ // Get the HTML-template from client. Overrides templateContents
+ // (even though both can never be given at the same time)
+ templateContents = getConnection().getResource(
+ "layouts/" + templateName + ".html");
+ if (templateContents == null) {
+ templateContents = "<em>Layout file layouts/"
+ + templateName
+ + ".html is missing. Components will be drawn for debug purposes.</em>";
+ }
+ }
+
+ getWidget().initializeHTML(templateContents,
+ getConnection().getThemeUri());
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ // Must do this once here so the HTML has been set up before we start
+ // adding child widgets.
+
+ updateHtmlTemplate();
+
+ // For all contained widgets
+ for (ComponentConnector child : getChildComponents()) {
+ String location = getState().getChildLocations().get(child);
+ try {
+ getWidget().setWidget(child.getWidget(), location);
+ } catch (final IllegalArgumentException e) {
+ // If no location is found, this component is not visible
+ }
+ }
+ for (ComponentConnector oldChild : event.getOldChildren()) {
+ if (oldChild.getParent() == this) {
+ // Connector still a child of this
+ continue;
+ }
+ Widget oldChildWidget = oldChild.getWidget();
+ if (oldChildWidget.isAttached()) {
+ // slot of this widget is emptied, remove it
+ getWidget().remove(oldChildWidget);
+ }
+ }
+
+ }
+
+ @Override
+ public VCustomLayout getWidget() {
+ return (VCustomLayout) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector paintable) {
+ getWidget().updateCaption(paintable);
+ }
+
+ @Override
+ public void layout() {
+ getWidget().iLayoutJS(DOM.getFirstChild(getWidget().getElement()));
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Not interested in anything from the UIDL - just implementing the
+ // interface to avoid some warning (#8688)
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java
new file mode 100644
index 0000000000..b4194c40a6
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/customlayout/VCustomLayout.java
@@ -0,0 +1,408 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.customlayout;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+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.ComponentConnector;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.VCaptionWrapper;
+
+/**
+ * Custom Layout implements complex layout defined with HTML template.
+ *
+ * @author Vaadin Ltd
+ *
+ */
+public class VCustomLayout extends ComplexPanel {
+
+ public static final String CLASSNAME = "v-customlayout";
+
+ /** Location-name to containing element in DOM map */
+ private final HashMap<String, Element> locationToElement = new HashMap<String, Element>();
+
+ /** Location-name to contained widget map */
+ final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>();
+
+ /** Widget to captionwrapper map */
+ private final HashMap<Widget, VCaptionWrapper> childWidgetToCaptionWrapper = new HashMap<Widget, VCaptionWrapper>();
+
+ /** Name of the currently rendered style */
+ String currentTemplateName;
+
+ /** Unexecuted scripts loaded from the template */
+ String scripts = "";
+
+ /** Paintable ID of this paintable */
+ String pid;
+
+ ApplicationConnection client;
+
+ private boolean htmlInitialized = false;
+
+ private Element elementWithNativeResizeFunction;
+
+ private String height = "";
+
+ private String width = "";
+
+ public VCustomLayout() {
+ 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 = 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);
+ }
+
+ /** Initialize HTML-layout. */
+ public void initializeHTML(String template, String themeUri) {
+
+ // 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 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);
+
+ htmlInitialized = true;
+ }
+
+ private native boolean uriEndsWithSlash()
+ /*-{
+ var path = $wnd.location.pathname;
+ if(path.charAt(path.length - 1) == "/")
+ return true;
+ return false;
+ }-*/;
+
+ boolean hasTemplate() {
+ return htmlInitialized;
+ }
+
+ /** Collect locations from template */
+ private void scanForLocations(Element elem) {
+
+ final String location = elem.getAttribute("location");
+ if (!"".equals(location)) {
+ locationToElement.put(location, elem);
+ elem.setInnerHTML("");
+
+ } 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 */
+ 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;
+ }
+
+ /** Update caption for given widget */
+ public void updateCaption(ComponentConnector paintable) {
+ Widget widget = paintable.getWidget();
+ VCaptionWrapper wrapper = childWidgetToCaptionWrapper.get(widget);
+ if (VCaption.isNeeded(paintable.getState())) {
+ if (wrapper == null) {
+ // Add a wrapper between the layout and the child widget
+ final String loc = getLocation(widget);
+ super.remove(widget);
+ wrapper = new VCaptionWrapper(paintable, client);
+ super.add(wrapper, locationToElement.get(loc));
+ childWidgetToCaptionWrapper.put(widget, wrapper);
+ }
+ wrapper.updateCaption();
+ } else {
+ if (wrapper != null) {
+ // Remove the wrapper and add the widget directly to the layout
+ final String loc = getLocation(widget);
+ super.remove(wrapper);
+ super.add(widget, locationToElement.get(loc));
+ childWidgetToCaptionWrapper.remove(widget);
+ }
+ }
+ }
+
+ /** Get the location of an widget */
+ public String getLocation(Widget w) {
+ for (final Iterator<String> i = locationToWidget.keySet().iterator(); i
+ .hasNext();) {
+ final String location = i.next();
+ if (locationToWidget.get(location) == w) {
+ return location;
+ }
+ }
+ return null;
+ }
+
+ /** Removes given widget from the layout */
+ @Override
+ public boolean remove(Widget w) {
+ final String location = getLocation(w);
+ if (location != null) {
+ locationToWidget.remove(location);
+ }
+ final VCaptionWrapper cw = childWidgetToCaptionWrapper.get(w);
+ if (cw != null) {
+ childWidgetToCaptionWrapper.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();
+ childWidgetToCaptionWrapper.clear();
+ }
+
+ /**
+ * 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();
+ if (elementWithNativeResizeFunction != null) {
+ detachResizedFunction(elementWithNativeResizeFunction);
+ }
+ }
+
+ private native void detachResizedFunction(Element element)
+ /*-{
+ element.notifyChildrenOfSizeChange = null;
+ }-*/;
+
+ private native void publishResizedFunction(Element element)
+ /*-{
+ var self = this;
+ element.notifyChildrenOfSizeChange = $entry(function() {
+ self.@com.vaadin.terminal.gwt.client.ui.customlayout.VCustomLayout::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.
+ */
+ native boolean iLayoutJS(Element el)
+ /*-{
+ if(el && el.iLayoutJS) {
+ try {
+ el.iLayoutJS();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }-*/;
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ event.cancelBubble(true);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java
new file mode 100644
index 0000000000..159b5bc414
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/AbstractDateFieldConnector.java
@@ -0,0 +1,111 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.LocaleNotLoadedException;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector;
+
+public class AbstractDateFieldConnector extends AbstractFieldConnector
+ implements Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // Save details
+ getWidget().client = client;
+ getWidget().paintableId = uidl.getId();
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().readonly = isReadOnly();
+ getWidget().enabled = isEnabled();
+
+ if (uidl.hasAttribute("locale")) {
+ final String locale = uidl.getStringAttribute("locale");
+ try {
+ getWidget().dts.setLocale(locale);
+ getWidget().currentLocale = locale;
+ } catch (final LocaleNotLoadedException e) {
+ getWidget().currentLocale = getWidget().dts.getLocale();
+ VConsole.error("Tried to use an unloaded locale \"" + locale
+ + "\". Using default locale ("
+ + getWidget().currentLocale + ").");
+ VConsole.error(e);
+ }
+ }
+
+ // We show week numbers only if the week starts with Monday, as ISO 8601
+ // specifies
+ getWidget().showISOWeekNumbers = uidl
+ .getBooleanAttribute(VDateField.WEEK_NUMBERS)
+ && getWidget().dts.getFirstDayOfWeek() == 1;
+
+ int newResolution;
+ if (uidl.hasVariable("sec")) {
+ newResolution = VDateField.RESOLUTION_SEC;
+ } else if (uidl.hasVariable("min")) {
+ newResolution = VDateField.RESOLUTION_MIN;
+ } else if (uidl.hasVariable("hour")) {
+ newResolution = VDateField.RESOLUTION_HOUR;
+ } else if (uidl.hasVariable("day")) {
+ newResolution = VDateField.RESOLUTION_DAY;
+ } else if (uidl.hasVariable("month")) {
+ newResolution = VDateField.RESOLUTION_MONTH;
+ } else {
+ newResolution = VDateField.RESOLUTION_YEAR;
+ }
+
+ // Remove old stylename that indicates current resolution
+ setWidgetStyleName(
+ VDateField.CLASSNAME
+ + "-"
+ + VDateField
+ .resolutionToString(getWidget().currentResolution),
+ false);
+
+ getWidget().currentResolution = newResolution;
+
+ // Add stylename that indicates current resolution
+ setWidgetStyleName(
+ VDateField.CLASSNAME
+ + "-"
+ + VDateField
+ .resolutionToString(getWidget().currentResolution),
+ true);
+
+ final int year = uidl.getIntVariable("year");
+ final int month = (getWidget().currentResolution >= VDateField.RESOLUTION_MONTH) ? uidl
+ .getIntVariable("month") : -1;
+ final int day = (getWidget().currentResolution >= VDateField.RESOLUTION_DAY) ? uidl
+ .getIntVariable("day") : -1;
+ final int hour = (getWidget().currentResolution >= VDateField.RESOLUTION_HOUR) ? uidl
+ .getIntVariable("hour") : 0;
+ final int min = (getWidget().currentResolution >= VDateField.RESOLUTION_MIN) ? uidl
+ .getIntVariable("min") : 0;
+ final int sec = (getWidget().currentResolution >= VDateField.RESOLUTION_SEC) ? uidl
+ .getIntVariable("sec") : 0;
+
+ // Construct new date for this datefield (only if not null)
+ if (year > -1) {
+ getWidget().setCurrentDate(
+ new Date((long) getWidget().getTime(year, month, day, hour,
+ min, sec, 0)));
+ } else {
+ getWidget().setCurrentDate(null);
+ }
+ }
+
+ @Override
+ public VDateField getWidget() {
+ return (VDateField) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java
new file mode 100644
index 0000000000..304c75322a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/InlineDateFieldConnector.java
@@ -0,0 +1,99 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusChangeListener;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.TimeChangeListener;
+import com.vaadin.ui.InlineDateField;
+
+@Connect(InlineDateField.class)
+public class InlineDateFieldConnector extends AbstractDateFieldConnector {
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ super.updateFromUIDL(uidl, client);
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().calendarPanel.setShowISOWeekNumbers(getWidget()
+ .isShowISOWeekNumbers());
+ getWidget().calendarPanel.setDateTimeService(getWidget()
+ .getDateTimeService());
+ getWidget().calendarPanel.setResolution(getWidget()
+ .getCurrentResolution());
+ Date currentDate = getWidget().getCurrentDate();
+ if (currentDate != null) {
+ getWidget().calendarPanel.setDate(new Date(currentDate.getTime()));
+ } else {
+ getWidget().calendarPanel.setDate(null);
+ }
+
+ if (getWidget().currentResolution > VDateField.RESOLUTION_DAY) {
+ getWidget().calendarPanel
+ .setTimeChangeListener(new TimeChangeListener() {
+ @Override
+ public void changed(int hour, int min, int sec, int msec) {
+ Date d = getWidget().getDate();
+ if (d == null) {
+ // date currently null, use the value from
+ // calendarPanel
+ // (~ client time at the init of the widget)
+ d = (Date) getWidget().calendarPanel.getDate()
+ .clone();
+ }
+ d.setHours(hour);
+ d.setMinutes(min);
+ d.setSeconds(sec);
+ DateTimeService.setMilliseconds(d, msec);
+
+ // Always update time changes to the server
+ getWidget().calendarPanel.setDate(d);
+ getWidget().updateValueFromPanel();
+ }
+ });
+ }
+
+ if (getWidget().currentResolution <= VDateField.RESOLUTION_MONTH) {
+ getWidget().calendarPanel
+ .setFocusChangeListener(new FocusChangeListener() {
+ @Override
+ public void focusChanged(Date date) {
+ Date date2 = new Date();
+ if (getWidget().calendarPanel.getDate() != null) {
+ date2.setTime(getWidget().calendarPanel
+ .getDate().getTime());
+ }
+ /*
+ * Update the value of calendarPanel
+ */
+ date2.setYear(date.getYear());
+ date2.setMonth(date.getMonth());
+ getWidget().calendarPanel.setDate(date2);
+ /*
+ * Then update the value from panel to server
+ */
+ getWidget().updateValueFromPanel();
+ }
+ });
+ } else {
+ getWidget().calendarPanel.setFocusChangeListener(null);
+ }
+
+ // Update possible changes
+ getWidget().calendarPanel.renderCalendar();
+ }
+
+ @Override
+ public VDateFieldCalendar getWidget() {
+ return (VDateFieldCalendar) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java
new file mode 100644
index 0000000000..a8c8ada1d9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/PopupDateFieldConnector.java
@@ -0,0 +1,137 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusChangeListener;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.TimeChangeListener;
+import com.vaadin.ui.DateField;
+
+@Connect(DateField.class)
+public class PopupDateFieldConnector extends TextualDateConnector {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.VTextualDate#updateFromUIDL(com.vaadin
+ * .terminal.gwt.client.UIDL,
+ * com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ @Override
+ @SuppressWarnings("deprecation")
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ boolean lastReadOnlyState = getWidget().readonly;
+ boolean lastEnabledState = getWidget().isEnabled();
+
+ getWidget().parsable = uidl.getBooleanAttribute("parsable");
+
+ super.updateFromUIDL(uidl, client);
+
+ getWidget().calendar.setDateTimeService(getWidget()
+ .getDateTimeService());
+ getWidget().calendar.setShowISOWeekNumbers(getWidget()
+ .isShowISOWeekNumbers());
+ if (getWidget().calendar.getResolution() != getWidget().currentResolution) {
+ getWidget().calendar.setResolution(getWidget().currentResolution);
+ if (getWidget().calendar.getDate() != null) {
+ getWidget().calendar.setDate((Date) getWidget()
+ .getCurrentDate().clone());
+ // force re-render when changing resolution only
+ getWidget().calendar.renderCalendar();
+ }
+ }
+ getWidget().calendarToggle.setEnabled(getWidget().enabled);
+
+ if (getWidget().currentResolution <= VPopupCalendar.RESOLUTION_MONTH) {
+ getWidget().calendar
+ .setFocusChangeListener(new FocusChangeListener() {
+ @Override
+ public void focusChanged(Date date) {
+ getWidget().updateValue(date);
+ getWidget().buildDate();
+ Date date2 = getWidget().calendar.getDate();
+ date2.setYear(date.getYear());
+ date2.setMonth(date.getMonth());
+ }
+ });
+ } else {
+ getWidget().calendar.setFocusChangeListener(null);
+ }
+
+ if (getWidget().currentResolution > VPopupCalendar.RESOLUTION_DAY) {
+ getWidget().calendar
+ .setTimeChangeListener(new TimeChangeListener() {
+ @Override
+ public void changed(int hour, int min, int sec, int msec) {
+ Date d = getWidget().getDate();
+ if (d == null) {
+ // date currently null, use the value from
+ // calendarPanel
+ // (~ client time at the init of the widget)
+ d = (Date) getWidget().calendar.getDate()
+ .clone();
+ }
+ d.setHours(hour);
+ d.setMinutes(min);
+ d.setSeconds(sec);
+ DateTimeService.setMilliseconds(d, msec);
+
+ // Always update time changes to the server
+ getWidget().updateValue(d);
+
+ // Update text field
+ getWidget().buildDate();
+ }
+ });
+ }
+
+ if (getWidget().readonly) {
+ getWidget().calendarToggle.addStyleName(VPopupCalendar.CLASSNAME
+ + "-button-readonly");
+ } else {
+ getWidget().calendarToggle.removeStyleName(VPopupCalendar.CLASSNAME
+ + "-button-readonly");
+ }
+
+ getWidget().calendarToggle.setEnabled(true);
+ }
+
+ @Override
+ public VPopupCalendar getWidget() {
+ return (VPopupCalendar) super.getWidget();
+ }
+
+ @Override
+ protected void setWidgetStyleName(String styleName, boolean add) {
+ super.setWidgetStyleName(styleName, add);
+
+ // update the style change to popup calendar widget
+ getWidget().popup.setStyleName(styleName, add);
+ }
+
+ @Override
+ protected void setWidgetStyleNameWithPrefix(String prefix,
+ String styleName, boolean add) {
+ super.setWidgetStyleNameWithPrefix(prefix, styleName, add);
+
+ // update the style change to popup calendar widget with the correct
+ // prefix
+ if (!styleName.startsWith("-")) {
+ getWidget().popup.setStyleName(
+ VPopupCalendar.POPUP_PRIMARY_STYLE_NAME + "-" + styleName,
+ add);
+ } else {
+ getWidget().popup.setStyleName(
+ VPopupCalendar.POPUP_PRIMARY_STYLE_NAME + styleName, add);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java
new file mode 100644
index 0000000000..32af4024cb
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/TextualDateConnector.java
@@ -0,0 +1,49 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class TextualDateConnector extends AbstractDateFieldConnector {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ int origRes = getWidget().currentResolution;
+ String oldLocale = getWidget().currentLocale;
+ super.updateFromUIDL(uidl, client);
+ if (origRes != getWidget().currentResolution
+ || oldLocale != getWidget().currentLocale) {
+ // force recreating format string
+ getWidget().formatStr = null;
+ }
+ if (uidl.hasAttribute("format")) {
+ getWidget().formatStr = uidl.getStringAttribute("format");
+ }
+
+ getWidget().inputPrompt = uidl
+ .getStringAttribute(VTextualDate.ATTR_INPUTPROMPT);
+
+ getWidget().lenient = !uidl.getBooleanAttribute("strict");
+
+ getWidget().buildDate();
+ // not a FocusWidget -> needs own tabindex handling
+ if (uidl.hasAttribute("tabindex")) {
+ getWidget().text.setTabIndex(uidl.getIntAttribute("tabindex"));
+ }
+
+ if (getWidget().readonly) {
+ getWidget().text.addStyleDependentName("readonly");
+ } else {
+ getWidget().text.removeStyleDependentName("readonly");
+ }
+
+ }
+
+ @Override
+ public VTextualDate getWidget() {
+ return (VTextualDate) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java
new file mode 100644
index 0000000000..e4e25a5a2e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VCalendarPanel.java
@@ -0,0 +1,1757 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+import java.util.Iterator;
+
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.InlineHTML;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.FocusableFlexTable;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.label.VLabel;
+import com.vaadin.terminal.gwt.client.ui.nativeselect.VNativeSelect;
+
+@SuppressWarnings("deprecation")
+public class VCalendarPanel extends FocusableFlexTable implements
+ KeyDownHandler, KeyPressHandler, MouseOutHandler, MouseDownHandler,
+ MouseUpHandler, BlurHandler, FocusHandler, SubPartAware {
+
+ public interface SubmitListener {
+
+ /**
+ * Called when calendar user triggers a submitting operation in calendar
+ * panel. Eg. clicking on day or hitting enter.
+ */
+ void onSubmit();
+
+ /**
+ * On eg. ESC key.
+ */
+ void onCancel();
+ }
+
+ /**
+ * Blur listener that listens to blur event from the panel
+ */
+ public interface FocusOutListener {
+ /**
+ * @return true if the calendar panel is not used after focus moves out
+ */
+ boolean onFocusOut(DomEvent<?> event);
+ }
+
+ /**
+ * FocusChangeListener is notified when the panel changes its _focused_
+ * value.
+ */
+ public interface FocusChangeListener {
+ void focusChanged(Date focusedDate);
+ }
+
+ /**
+ * Dispatches an event when the panel when time is changed
+ */
+ public interface TimeChangeListener {
+
+ void changed(int hour, int min, int sec, int msec);
+ }
+
+ /**
+ * Represents a Date button in the calendar
+ */
+ private class VEventButton extends Button {
+ public VEventButton() {
+ addMouseDownHandler(VCalendarPanel.this);
+ addMouseOutHandler(VCalendarPanel.this);
+ addMouseUpHandler(VCalendarPanel.this);
+ }
+ }
+
+ private static final String CN_FOCUSED = "focused";
+
+ private static final String CN_TODAY = "today";
+
+ private static final String CN_SELECTED = "selected";
+
+ private static final String CN_OFFMONTH = "offmonth";
+
+ /**
+ * Represents a click handler for when a user selects a value by using the
+ * mouse
+ */
+ private ClickHandler dayClickHandler = new ClickHandler() {
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt
+ * .event.dom.client.ClickEvent)
+ */
+ @Override
+ public void onClick(ClickEvent event) {
+ Day day = (Day) event.getSource();
+ focusDay(day.getDate());
+ selectFocused();
+ onSubmit();
+ }
+ };
+
+ private VEventButton prevYear;
+
+ private VEventButton nextYear;
+
+ private VEventButton prevMonth;
+
+ private VEventButton nextMonth;
+
+ private VTime time;
+
+ private FlexTable days = new FlexTable();
+
+ private int resolution = VDateField.RESOLUTION_YEAR;
+
+ private int focusedRow;
+
+ private Timer mouseTimer;
+
+ private Date value;
+
+ private boolean enabled = true;
+
+ private boolean readonly = false;
+
+ private DateTimeService dateTimeService;
+
+ private boolean showISOWeekNumbers;
+
+ private Date displayedMonth;
+
+ private Date focusedDate;
+
+ private Day selectedDay;
+
+ private Day focusedDay;
+
+ private FocusOutListener focusOutListener;
+
+ private SubmitListener submitListener;
+
+ private FocusChangeListener focusChangeListener;
+
+ private TimeChangeListener timeChangeListener;
+
+ private boolean hasFocus = false;
+
+ public VCalendarPanel() {
+
+ setStyleName(VDateField.CLASSNAME + "-calendarpanel");
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ addKeyPressHandler(this);
+ } else {
+ addKeyDownHandler(this);
+ }
+ addFocusHandler(this);
+ addBlurHandler(this);
+
+ }
+
+ /**
+ * Sets the focus to given date in the current view. Used when moving in the
+ * calendar with the keyboard.
+ *
+ * @param date
+ * A Date representing the day of month to be focused. Must be
+ * one of the days currently visible.
+ */
+ private void focusDay(Date date) {
+ // Only used when calender body is present
+ if (resolution > VDateField.RESOLUTION_MONTH) {
+ if (focusedDay != null) {
+ focusedDay.removeStyleDependentName(CN_FOCUSED);
+ }
+
+ if (date != null && focusedDate != null) {
+ focusedDate.setTime(date.getTime());
+ int rowCount = days.getRowCount();
+ for (int i = 0; i < rowCount; i++) {
+ int cellCount = days.getCellCount(i);
+ for (int j = 0; j < cellCount; j++) {
+ Widget widget = days.getWidget(i, j);
+ if (widget != null && widget instanceof Day) {
+ Day curday = (Day) widget;
+ if (curday.getDate().equals(date)) {
+ curday.addStyleDependentName(CN_FOCUSED);
+ focusedDay = curday;
+ focusedRow = i;
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the selection highlight to a given day in the current view
+ *
+ * @param date
+ * A Date representing the day of month to be selected. Must be
+ * one of the days currently visible.
+ *
+ */
+ private void selectDate(Date date) {
+ if (selectedDay != null) {
+ selectedDay.removeStyleDependentName(CN_SELECTED);
+ }
+
+ int rowCount = days.getRowCount();
+ for (int i = 0; i < rowCount; i++) {
+ int cellCount = days.getCellCount(i);
+ for (int j = 0; j < cellCount; j++) {
+ Widget widget = days.getWidget(i, j);
+ if (widget != null && widget instanceof Day) {
+ Day curday = (Day) widget;
+ if (curday.getDate().equals(date)) {
+ curday.addStyleDependentName(CN_SELECTED);
+ selectedDay = curday;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates year, month, day from focusedDate to value
+ */
+ private void selectFocused() {
+ if (focusedDate != null) {
+ if (value == null) {
+ // No previously selected value (set to null on server side).
+ // Create a new date using current date and time
+ value = new Date();
+ }
+ /*
+ * #5594 set Date (day) to 1 in order to prevent any kind of
+ * wrapping of months when later setting the month. (e.g. 31 ->
+ * month with 30 days -> wraps to the 1st of the following month,
+ * e.g. 31st of May -> 31st of April = 1st of May)
+ */
+ value.setDate(1);
+ if (value.getYear() != focusedDate.getYear()) {
+ value.setYear(focusedDate.getYear());
+ }
+ if (value.getMonth() != focusedDate.getMonth()) {
+ value.setMonth(focusedDate.getMonth());
+ }
+ if (value.getDate() != focusedDate.getDate()) {
+ }
+ // We always need to set the date, even if it hasn't changed, since
+ // it was forced to 1 above.
+ value.setDate(focusedDate.getDate());
+
+ selectDate(focusedDate);
+ } else {
+ VConsole.log("Trying to select a the focused date which is NULL!");
+ }
+ }
+
+ protected boolean onValueChange() {
+ return false;
+ }
+
+ public int getResolution() {
+ return resolution;
+ }
+
+ public void setResolution(int resolution) {
+ this.resolution = resolution;
+ if (time != null) {
+ time.removeFromParent();
+ time = null;
+ }
+ }
+
+ private boolean isReadonly() {
+ return readonly;
+ }
+
+ private boolean isEnabled() {
+ return enabled;
+ }
+
+ private void clearCalendarBody(boolean remove) {
+ if (!remove) {
+ // Leave the cells in place but clear their contents
+
+ // This has the side effect of ensuring that the calendar always
+ // contain 7 rows.
+ for (int row = 1; row < 7; row++) {
+ for (int col = 0; col < 8; col++) {
+ days.setHTML(row, col, "&nbsp;");
+ }
+ }
+ } else if (getRowCount() > 1) {
+ removeRow(1);
+ days.clear();
+ }
+ }
+
+ /**
+ * Builds the top buttons and current month and year header.
+ *
+ * @param needsMonth
+ * Should the month buttons be visible?
+ */
+ private void buildCalendarHeader(boolean needsMonth) {
+
+ getRowFormatter().addStyleName(0,
+ VDateField.CLASSNAME + "-calendarpanel-header");
+
+ if (prevMonth == null && needsMonth) {
+ prevMonth = new VEventButton();
+ prevMonth.setHTML("&lsaquo;");
+ prevMonth.setStyleName("v-button-prevmonth");
+ prevMonth.setTabIndex(-1);
+ nextMonth = new VEventButton();
+ nextMonth.setHTML("&rsaquo;");
+ nextMonth.setStyleName("v-button-nextmonth");
+ nextMonth.setTabIndex(-1);
+ getFlexCellFormatter().setStyleName(0, 3,
+ VDateField.CLASSNAME + "-calendarpanel-nextmonth");
+ getFlexCellFormatter().setStyleName(0, 1,
+ VDateField.CLASSNAME + "-calendarpanel-prevmonth");
+
+ setWidget(0, 3, nextMonth);
+ setWidget(0, 1, prevMonth);
+ } else if (prevMonth != null && !needsMonth) {
+ // Remove month traverse buttons
+ remove(prevMonth);
+ remove(nextMonth);
+ prevMonth = null;
+ nextMonth = null;
+ }
+
+ if (prevYear == null) {
+ prevYear = new VEventButton();
+ prevYear.setHTML("&laquo;");
+ prevYear.setStyleName("v-button-prevyear");
+ prevYear.setTabIndex(-1);
+ nextYear = new VEventButton();
+ nextYear.setHTML("&raquo;");
+ nextYear.setStyleName("v-button-nextyear");
+ nextYear.setTabIndex(-1);
+ setWidget(0, 0, prevYear);
+ setWidget(0, 4, nextYear);
+ getFlexCellFormatter().setStyleName(0, 0,
+ VDateField.CLASSNAME + "-calendarpanel-prevyear");
+ getFlexCellFormatter().setStyleName(0, 4,
+ VDateField.CLASSNAME + "-calendarpanel-nextyear");
+ }
+
+ final String monthName = needsMonth ? getDateTimeService().getMonth(
+ focusedDate.getMonth()) : "";
+ final int year = focusedDate.getYear() + 1900;
+ getFlexCellFormatter().setStyleName(0, 2,
+ VDateField.CLASSNAME + "-calendarpanel-month");
+ setHTML(0, 2, "<span class=\"" + VDateField.CLASSNAME
+ + "-calendarpanel-month\">" + monthName + " " + year
+ + "</span>");
+ }
+
+ private DateTimeService getDateTimeService() {
+ return dateTimeService;
+ }
+
+ public void setDateTimeService(DateTimeService dateTimeService) {
+ this.dateTimeService = dateTimeService;
+ }
+
+ /**
+ * Returns whether ISO 8601 week numbers should be shown in the value
+ * selector or not. ISO 8601 defines that a week always starts with a Monday
+ * so the week numbers are only shown if this is the case.
+ *
+ * @return true if week number should be shown, false otherwise
+ */
+ public boolean isShowISOWeekNumbers() {
+ return showISOWeekNumbers;
+ }
+
+ public void setShowISOWeekNumbers(boolean showISOWeekNumbers) {
+ this.showISOWeekNumbers = showISOWeekNumbers;
+ }
+
+ /**
+ * Builds the day and time selectors of the calendar.
+ */
+ private void buildCalendarBody() {
+
+ final int weekColumn = 0;
+ final int firstWeekdayColumn = 1;
+ final int headerRow = 0;
+
+ setWidget(1, 0, days);
+ setCellPadding(0);
+ setCellSpacing(0);
+ getFlexCellFormatter().setColSpan(1, 0, 5);
+ getFlexCellFormatter().setStyleName(1, 0,
+ VDateField.CLASSNAME + "-calendarpanel-body");
+
+ days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
+ "v-week");
+ days.setHTML(headerRow, weekColumn, "<strong></strong>");
+ // Hide the week column if week numbers are not to be displayed.
+ days.getFlexCellFormatter().setVisible(headerRow, weekColumn,
+ isShowISOWeekNumbers());
+
+ days.getRowFormatter().setStyleName(headerRow,
+ VDateField.CLASSNAME + "-calendarpanel-weekdays");
+
+ if (isShowISOWeekNumbers()) {
+ days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
+ "v-first");
+ days.getFlexCellFormatter().setStyleName(headerRow,
+ firstWeekdayColumn, "");
+ days.getRowFormatter().addStyleName(headerRow,
+ VDateField.CLASSNAME + "-calendarpanel-weeknumbers");
+ } else {
+ days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, "");
+ days.getFlexCellFormatter().setStyleName(headerRow,
+ firstWeekdayColumn, "v-first");
+ }
+
+ days.getFlexCellFormatter().setStyleName(headerRow,
+ firstWeekdayColumn + 6, "v-last");
+
+ // Print weekday names
+ final int firstDay = getDateTimeService().getFirstDayOfWeek();
+ for (int i = 0; i < 7; i++) {
+ int day = i + firstDay;
+ if (day > 6) {
+ day = 0;
+ }
+ if (getResolution() > VDateField.RESOLUTION_MONTH) {
+ days.setHTML(headerRow, firstWeekdayColumn + i, "<strong>"
+ + getDateTimeService().getShortDay(day) + "</strong>");
+ } else {
+ days.setHTML(headerRow, firstWeekdayColumn + i, "");
+ }
+ }
+
+ // Zero out hours, minutes, seconds, and milliseconds to compare dates
+ // without time part
+ final Date tmp = new Date();
+ final Date today = new Date(tmp.getYear(), tmp.getMonth(),
+ tmp.getDate());
+
+ final Date selectedDate = value == null ? null : new Date(
+ value.getYear(), value.getMonth(), value.getDate());
+
+ final int startWeekDay = getDateTimeService().getStartWeekDay(
+ displayedMonth);
+ final Date curr = (Date) displayedMonth.clone();
+ // Start from the first day of the week that at least partially belongs
+ // to the current month
+ curr.setDate(1 - startWeekDay);
+
+ // No month has more than 6 weeks so 6 is a safe maximum for rows.
+ for (int weekOfMonth = 1; weekOfMonth < 7; weekOfMonth++) {
+ for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
+
+ // Actually write the day of month
+ Day day = new Day((Date) curr.clone());
+
+ if (curr.equals(selectedDate)) {
+ day.addStyleDependentName(CN_SELECTED);
+ selectedDay = day;
+ }
+ if (curr.equals(today)) {
+ day.addStyleDependentName(CN_TODAY);
+ }
+ if (curr.equals(focusedDate)) {
+ focusedDay = day;
+ focusedRow = weekOfMonth;
+ if (hasFocus) {
+ day.addStyleDependentName(CN_FOCUSED);
+ }
+ }
+ if (curr.getMonth() != displayedMonth.getMonth()) {
+ day.addStyleDependentName(CN_OFFMONTH);
+ }
+
+ days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek, day);
+
+ // ISO week numbers if requested
+ days.getCellFormatter().setVisible(weekOfMonth, weekColumn,
+ isShowISOWeekNumbers());
+ if (isShowISOWeekNumbers()) {
+ final String baseCssClass = VDateField.CLASSNAME
+ + "-calendarpanel-weeknumber";
+ String weekCssClass = baseCssClass;
+
+ int weekNumber = DateTimeService.getISOWeekNumber(curr);
+
+ days.setHTML(weekOfMonth, 0, "<span class=\""
+ + weekCssClass + "\"" + ">" + weekNumber
+ + "</span>");
+ }
+ curr.setDate(curr.getDate() + 1);
+ }
+ }
+ }
+
+ /**
+ * Do we need the time selector
+ *
+ * @return True if it is required
+ */
+ private boolean isTimeSelectorNeeded() {
+ return getResolution() > VDateField.RESOLUTION_DAY;
+ }
+
+ /**
+ * Updates the calendar and text field with the selected dates.
+ */
+ public void renderCalendar() {
+ if (focusedDate == null) {
+ Date now = new Date();
+ // focusedDate must have zero hours, mins, secs, millisecs
+ focusedDate = new Date(now.getYear(), now.getMonth(), now.getDate());
+ displayedMonth = new Date(now.getYear(), now.getMonth(), 1);
+ }
+
+ if (getResolution() <= VDateField.RESOLUTION_MONTH
+ && focusChangeListener != null) {
+ focusChangeListener.focusChanged(new Date(focusedDate.getTime()));
+ }
+
+ final boolean needsMonth = getResolution() > VDateField.RESOLUTION_YEAR;
+ boolean needsBody = getResolution() >= VDateField.RESOLUTION_DAY;
+ buildCalendarHeader(needsMonth);
+ clearCalendarBody(!needsBody);
+ if (needsBody) {
+ buildCalendarBody();
+ }
+
+ if (isTimeSelectorNeeded() && time == null) {
+ time = new VTime();
+ setWidget(2, 0, time);
+ getFlexCellFormatter().setColSpan(2, 0, 5);
+ getFlexCellFormatter().setStyleName(2, 0,
+ VDateField.CLASSNAME + "-calendarpanel-time");
+ } else if (isTimeSelectorNeeded()) {
+ time.updateTimes();
+ } else if (time != null) {
+ remove(time);
+ }
+ }
+
+ /**
+ * Moves the focus forward the given number of days.
+ */
+ private void focusNextDay(int days) {
+ int oldMonth = focusedDate.getMonth();
+ focusedDate.setDate(focusedDate.getDate() + days);
+
+ if (focusedDate.getMonth() == oldMonth) {
+ // Month did not change, only move the selection
+ focusDay(focusedDate);
+ } else {
+ // If the month changed we need to re-render the calendar
+ displayedMonth.setMonth(focusedDate.getMonth());
+ renderCalendar();
+ }
+ }
+
+ /**
+ * Moves the focus backward the given number of days.
+ */
+ private void focusPreviousDay(int days) {
+ focusNextDay(-days);
+ }
+
+ /**
+ * Selects the next month
+ */
+ private void focusNextMonth() {
+
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setMonth(currentMonth + 1);
+ int requestedMonth = (currentMonth + 1) % 12;
+
+ /*
+ * If the selected value was e.g. 31.3 the new value would be 31.4 but
+ * this value is invalid so the new value will be 1.5. This is taken
+ * care of by decreasing the value until we have the correct month.
+ */
+ while (focusedDate.getMonth() != requestedMonth) {
+ focusedDate.setDate(focusedDate.getDate() - 1);
+ }
+ displayedMonth.setMonth(displayedMonth.getMonth() + 1);
+
+ renderCalendar();
+ }
+
+ /**
+ * Selects the previous month
+ */
+ private void focusPreviousMonth() {
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setMonth(currentMonth - 1);
+
+ /*
+ * If the selected value was e.g. 31.12 the new value would be 31.11 but
+ * this value is invalid so the new value will be 1.12. This is taken
+ * care of by decreasing the value until we have the correct month.
+ */
+ while (focusedDate.getMonth() == currentMonth) {
+ focusedDate.setDate(focusedDate.getDate() - 1);
+ }
+ displayedMonth.setMonth(displayedMonth.getMonth() - 1);
+
+ renderCalendar();
+ }
+
+ /**
+ * Selects the previous year
+ */
+ private void focusPreviousYear(int years) {
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setYear(focusedDate.getYear() - years);
+ displayedMonth.setYear(displayedMonth.getYear() - years);
+ /*
+ * If the focused date was a leap day (Feb 29), the new date becomes Mar
+ * 1 if the new year is not also a leap year. Set it to Feb 28 instead.
+ */
+ if (focusedDate.getMonth() != currentMonth) {
+ focusedDate.setDate(0);
+ }
+ renderCalendar();
+ }
+
+ /**
+ * Selects the next year
+ */
+ private void focusNextYear(int years) {
+ int currentMonth = focusedDate.getMonth();
+ focusedDate.setYear(focusedDate.getYear() + years);
+ displayedMonth.setYear(displayedMonth.getYear() + years);
+ /*
+ * If the focused date was a leap day (Feb 29), the new date becomes Mar
+ * 1 if the new year is not also a leap year. Set it to Feb 28 instead.
+ */
+ if (focusedDate.getMonth() != currentMonth) {
+ focusedDate.setDate(0);
+ }
+ renderCalendar();
+ }
+
+ /**
+ * Handles a user click on the component
+ *
+ * @param sender
+ * The component that was clicked
+ * @param updateVariable
+ * Should the value field be updated
+ *
+ */
+ private void processClickEvent(Widget sender) {
+ if (!isEnabled() || isReadonly()) {
+ return;
+ }
+ if (sender == prevYear) {
+ focusPreviousYear(1);
+ } else if (sender == nextYear) {
+ focusNextYear(1);
+ } else if (sender == prevMonth) {
+ focusPreviousMonth();
+ } else if (sender == nextMonth) {
+ focusNextMonth();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ handleKeyPress(event);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
+ * .gwt.event.dom.client.KeyPressEvent)
+ */
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ handleKeyPress(event);
+ }
+
+ /**
+ * Handles the keypress from both the onKeyPress event and the onKeyDown
+ * event
+ *
+ * @param event
+ * The keydown/keypress event
+ */
+ private void handleKeyPress(DomEvent<?> event) {
+ if (time != null
+ && time.getElement().isOrHasChild(
+ (Node) event.getNativeEvent().getEventTarget().cast())) {
+ int nativeKeyCode = event.getNativeEvent().getKeyCode();
+ if (nativeKeyCode == getSelectKey()) {
+ onSubmit(); // submit happens if enter key hit down on listboxes
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ return;
+ }
+
+ // Check tabs
+ int keycode = event.getNativeEvent().getKeyCode();
+ if (keycode == KeyCodes.KEY_TAB && event.getNativeEvent().getShiftKey()) {
+ if (onTabOut(event)) {
+ return;
+ }
+ }
+
+ // Handle the navigation
+ if (handleNavigation(keycode, event.getNativeEvent().getCtrlKey()
+ || event.getNativeEvent().getMetaKey(), event.getNativeEvent()
+ .getShiftKey())) {
+ event.preventDefault();
+ }
+
+ }
+
+ /**
+ * Notifies submit-listeners of a submit event
+ */
+ private void onSubmit() {
+ if (getSubmitListener() != null) {
+ getSubmitListener().onSubmit();
+ }
+ }
+
+ /**
+ * Notifies submit-listeners of a cancel event
+ */
+ private void onCancel() {
+ if (getSubmitListener() != null) {
+ getSubmitListener().onCancel();
+ }
+ }
+
+ /**
+ * Handles the keyboard navigation when the resolution is set to years.
+ *
+ * @param keycode
+ * The keycode to process
+ * @param ctrl
+ * Is ctrl pressed?
+ * @param shift
+ * is shift pressed
+ * @return Returns true if the keycode was processed, else false
+ */
+ protected boolean handleNavigationYearMode(int keycode, boolean ctrl,
+ boolean shift) {
+
+ // Ctrl and Shift selection not supported
+ if (ctrl || shift) {
+ return false;
+ }
+
+ else if (keycode == getPreviousKey()) {
+ focusNextYear(10); // Add 10 years
+ return true;
+ }
+
+ else if (keycode == getForwardKey()) {
+ focusNextYear(1); // Add 1 year
+ return true;
+ }
+
+ else if (keycode == getNextKey()) {
+ focusPreviousYear(10); // Subtract 10 years
+ return true;
+ }
+
+ else if (keycode == getBackwardKey()) {
+ focusPreviousYear(1); // Subtract 1 year
+ return true;
+
+ } else if (keycode == getSelectKey()) {
+ value = (Date) focusedDate.clone();
+ onSubmit();
+ return true;
+
+ } else if (keycode == getResetKey()) {
+ // Restore showing value the selected value
+ focusedDate.setTime(value.getTime());
+ renderCalendar();
+ return true;
+
+ } else if (keycode == getCloseKey()) {
+ // TODO fire listener, on users responsibility??
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle the keyboard navigation when the resolution is set to MONTH
+ *
+ * @param keycode
+ * The keycode to handle
+ * @param ctrl
+ * Was the ctrl key pressed?
+ * @param shift
+ * Was the shift key pressed?
+ * @return
+ */
+ protected boolean handleNavigationMonthMode(int keycode, boolean ctrl,
+ boolean shift) {
+
+ // Ctrl selection not supported
+ if (ctrl) {
+ return false;
+
+ } else if (keycode == getPreviousKey()) {
+ focusNextYear(1); // Add 1 year
+ return true;
+
+ } else if (keycode == getForwardKey()) {
+ focusNextMonth(); // Add 1 month
+ return true;
+
+ } else if (keycode == getNextKey()) {
+ focusPreviousYear(1); // Subtract 1 year
+ return true;
+
+ } else if (keycode == getBackwardKey()) {
+ focusPreviousMonth(); // Subtract 1 month
+ return true;
+
+ } else if (keycode == getSelectKey()) {
+ value = (Date) focusedDate.clone();
+ onSubmit();
+ return true;
+
+ } else if (keycode == getResetKey()) {
+ // Restore showing value the selected value
+ focusedDate.setTime(value.getTime());
+ renderCalendar();
+ return true;
+
+ } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) {
+
+ // TODO fire close event
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle keyboard navigation what the resolution is set to DAY
+ *
+ * @param keycode
+ * The keycode to handle
+ * @param ctrl
+ * Was the ctrl key pressed?
+ * @param shift
+ * Was the shift key pressed?
+ * @return Return true if the key press was handled by the method, else
+ * return false.
+ */
+ protected boolean handleNavigationDayMode(int keycode, boolean ctrl,
+ boolean shift) {
+
+ // Ctrl key is not in use
+ if (ctrl) {
+ return false;
+ }
+
+ /*
+ * Jumps to the next day.
+ */
+ if (keycode == getForwardKey() && !shift) {
+ focusNextDay(1);
+ return true;
+
+ /*
+ * Jumps to the previous day
+ */
+ } else if (keycode == getBackwardKey() && !shift) {
+ focusPreviousDay(1);
+ return true;
+
+ /*
+ * Jumps one week forward in the calendar
+ */
+ } else if (keycode == getNextKey() && !shift) {
+ focusNextDay(7);
+ return true;
+
+ /*
+ * Jumps one week back in the calendar
+ */
+ } else if (keycode == getPreviousKey() && !shift) {
+ focusPreviousDay(7);
+ return true;
+
+ /*
+ * Selects the value that is chosen
+ */
+ } else if (keycode == getSelectKey() && !shift) {
+ selectFocused();
+ onSubmit(); // submit
+ return true;
+
+ } else if (keycode == getCloseKey()) {
+ onCancel();
+ // TODO close event
+
+ return true;
+
+ /*
+ * Jumps to the next month
+ */
+ } else if (shift && keycode == getForwardKey()) {
+ focusNextMonth();
+ return true;
+
+ /*
+ * Jumps to the previous month
+ */
+ } else if (shift && keycode == getBackwardKey()) {
+ focusPreviousMonth();
+ return true;
+
+ /*
+ * Jumps to the next year
+ */
+ } else if (shift && keycode == getPreviousKey()) {
+ focusNextYear(1);
+ return true;
+
+ /*
+ * Jumps to the previous year
+ */
+ } else if (shift && keycode == getNextKey()) {
+ focusPreviousYear(1);
+ return true;
+
+ /*
+ * Resets the selection
+ */
+ } else if (keycode == getResetKey() && !shift) {
+ // Restore showing value the selected value
+ focusedDate = new Date(value.getYear(), value.getMonth(),
+ value.getDate());
+ displayedMonth = new Date(value.getYear(), value.getMonth(), 1);
+ renderCalendar();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles the keyboard navigation
+ *
+ * @param keycode
+ * The key code that was pressed
+ * @param ctrl
+ * Was the ctrl key pressed
+ * @param shift
+ * Was the shift key pressed
+ * @return Return true if key press was handled by the component, else
+ * return false
+ */
+ protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+ if (!isEnabled() || isReadonly()) {
+ return false;
+ }
+
+ else if (resolution == VDateField.RESOLUTION_YEAR) {
+ return handleNavigationYearMode(keycode, ctrl, shift);
+ }
+
+ else if (resolution == VDateField.RESOLUTION_MONTH) {
+ return handleNavigationMonthMode(keycode, ctrl, shift);
+ }
+
+ else if (resolution == VDateField.RESOLUTION_DAY) {
+ return handleNavigationDayMode(keycode, ctrl, shift);
+ }
+
+ else {
+ return handleNavigationDayMode(keycode, ctrl, shift);
+ }
+
+ }
+
+ /**
+ * Returns the reset key which will reset the calendar to the previous
+ * selection. By default this is backspace but it can be overriden to change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getResetKey() {
+ return KeyCodes.KEY_BACKSPACE;
+ }
+
+ /**
+ * Returns the select key which selects the value. By default this is the
+ * enter key but it can be changed to whatever you like by overriding this
+ * method.
+ *
+ * @return
+ */
+ protected int getSelectKey() {
+ return KeyCodes.KEY_ENTER;
+ }
+
+ /**
+ * Returns the key that closes the popup window if this is a VPopopCalendar.
+ * Else this does nothing. By default this is the Escape key but you can
+ * change the key to whatever you want by overriding this method.
+ *
+ * @return
+ */
+ protected int getCloseKey() {
+ return KeyCodes.KEY_ESCAPE;
+ }
+
+ /**
+ * The key that selects the next day in the calendar. By default this is the
+ * right arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getForwardKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * The key that selects the previous day in the calendar. By default this is
+ * the left arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getBackwardKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * The key that selects the next week in the calendar. By default this is
+ * the down arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getNextKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * The key that selects the previous week in the calendar. By default this
+ * is the up arrow key but by overriding this method it can be changed to
+ * whatever you like.
+ *
+ * @return
+ */
+ protected int getPreviousKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google
+ * .gwt.event.dom.client.MouseOutEvent)
+ */
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ if (mouseTimer != null) {
+ mouseTimer.cancel();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google
+ * .gwt.event.dom.client.MouseDownEvent)
+ */
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ // 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 (event.getSource() instanceof VEventButton) {
+ final VEventButton sender = (VEventButton) event.getSource();
+ processClickEvent(sender);
+ mouseTimer = new Timer() {
+ @Override
+ public void run() {
+ mouseTimer = new Timer() {
+ @Override
+ public void run() {
+ processClickEvent(sender);
+ }
+ };
+ mouseTimer.scheduleRepeating(150);
+ }
+ };
+ mouseTimer.schedule(500);
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseUpHandler#onMouseUp(com.google.gwt
+ * .event.dom.client.MouseUpEvent)
+ */
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ if (mouseTimer != null) {
+ mouseTimer.cancel();
+ }
+ }
+
+ /**
+ * Sets the data of the Panel.
+ *
+ * @param currentDate
+ * The date to set
+ */
+ public void setDate(Date currentDate) {
+
+ // Check that we are not re-rendering an already active date
+ if (currentDate == value && currentDate != null) {
+ return;
+ }
+
+ Date oldDisplayedMonth = displayedMonth;
+ value = currentDate;
+
+ if (value == null) {
+ focusedDate = displayedMonth = null;
+ } else {
+ focusedDate = new Date(value.getYear(), value.getMonth(),
+ value.getDate());
+ displayedMonth = new Date(value.getYear(), value.getMonth(), 1);
+ }
+
+ // Re-render calendar if the displayed month is changed,
+ // or if a time selector is needed but does not exist.
+ if ((isTimeSelectorNeeded() && time == null)
+ || oldDisplayedMonth == null || value == null
+ || oldDisplayedMonth.getYear() != value.getYear()
+ || oldDisplayedMonth.getMonth() != value.getMonth()) {
+ renderCalendar();
+ } else {
+ focusDay(focusedDate);
+ selectFocused();
+ if (isTimeSelectorNeeded()) {
+ time.updateTimes();
+ }
+ }
+
+ if (!hasFocus) {
+ focusDay(null);
+ }
+ }
+
+ /**
+ * TimeSelector is a widget consisting of list boxes that modifie the Date
+ * object that is given for.
+ *
+ */
+ public class VTime extends FlowPanel implements ChangeHandler {
+
+ private ListBox hours;
+
+ private ListBox mins;
+
+ private ListBox sec;
+
+ private ListBox ampm;
+
+ /**
+ * Constructor
+ */
+ public VTime() {
+ super();
+ setStyleName(VDateField.CLASSNAME + "-time");
+ buildTime();
+ }
+
+ private ListBox createListBox() {
+ ListBox lb = new ListBox();
+ lb.setStyleName(VNativeSelect.CLASSNAME);
+ lb.addChangeHandler(this);
+ lb.addBlurHandler(VCalendarPanel.this);
+ lb.addFocusHandler(VCalendarPanel.this);
+ return lb;
+ }
+
+ /**
+ * Constructs the ListBoxes and updates their value
+ *
+ * @param redraw
+ * Should new instances of the listboxes be created
+ */
+ private void buildTime() {
+ clear();
+
+ hours = createListBox();
+ if (getDateTimeService().isTwelveHourClock()) {
+ hours.addItem("12");
+ for (int i = 1; i < 12; i++) {
+ hours.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ } else {
+ for (int i = 0; i < 24; i++) {
+ hours.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ }
+
+ hours.addChangeHandler(this);
+ if (getDateTimeService().isTwelveHourClock()) {
+ ampm = createListBox();
+ final String[] ampmText = getDateTimeService().getAmPmStrings();
+ ampm.addItem(ampmText[0]);
+ ampm.addItem(ampmText[1]);
+ ampm.addChangeHandler(this);
+ }
+
+ if (getResolution() >= VDateField.RESOLUTION_MIN) {
+ mins = createListBox();
+ for (int i = 0; i < 60; i++) {
+ mins.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ mins.addChangeHandler(this);
+ }
+ if (getResolution() >= VDateField.RESOLUTION_SEC) {
+ sec = createListBox();
+ for (int i = 0; i < 60; i++) {
+ sec.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ sec.addChangeHandler(this);
+ }
+
+ final String delimiter = getDateTimeService().getClockDelimeter();
+ if (isReadonly()) {
+ int h = 0;
+ if (value != null) {
+ h = value.getHours();
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ h -= h < 12 ? 0 : 12;
+ }
+ add(new VLabel(h < 10 ? "0" + h : "" + h));
+ } else {
+ add(hours);
+ }
+
+ if (getResolution() >= VDateField.RESOLUTION_MIN) {
+ add(new VLabel(delimiter));
+ if (isReadonly()) {
+ final int m = mins.getSelectedIndex();
+ add(new VLabel(m < 10 ? "0" + m : "" + m));
+ } else {
+ add(mins);
+ }
+ }
+ if (getResolution() >= VDateField.RESOLUTION_SEC) {
+ add(new VLabel(delimiter));
+ if (isReadonly()) {
+ final int s = sec.getSelectedIndex();
+ add(new VLabel(s < 10 ? "0" + s : "" + s));
+ } else {
+ add(sec);
+ }
+ }
+ if (getResolution() == VDateField.RESOLUTION_HOUR) {
+ add(new VLabel(delimiter + "00")); // o'clock
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ add(new VLabel("&nbsp;"));
+ if (isReadonly()) {
+ int i = 0;
+ if (value != null) {
+ i = (value.getHours() < 12) ? 0 : 1;
+ }
+ add(new VLabel(ampm.getItemText(i)));
+ } else {
+ add(ampm);
+ }
+ }
+
+ if (isReadonly()) {
+ return;
+ }
+
+ // Update times
+ updateTimes();
+
+ ListBox lastDropDown = getLastDropDown();
+ lastDropDown.addKeyDownHandler(new KeyDownHandler() {
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ boolean shiftKey = event.getNativeEvent().getShiftKey();
+ if (shiftKey) {
+ return;
+ } else {
+ int nativeKeyCode = event.getNativeKeyCode();
+ if (nativeKeyCode == KeyCodes.KEY_TAB) {
+ onTabOut(event);
+ }
+ }
+ }
+ });
+
+ }
+
+ private ListBox getLastDropDown() {
+ int i = getWidgetCount() - 1;
+ while (i >= 0) {
+ Widget widget = getWidget(i);
+ if (widget instanceof ListBox) {
+ return (ListBox) widget;
+ }
+ i--;
+ }
+ return null;
+ }
+
+ /**
+ * Updates the valus to correspond to the values in value
+ */
+ public void updateTimes() {
+ boolean selected = true;
+ if (value == null) {
+ value = new Date();
+ selected = false;
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ int h = value.getHours();
+ ampm.setSelectedIndex(h < 12 ? 0 : 1);
+ h -= ampm.getSelectedIndex() * 12;
+ hours.setSelectedIndex(h);
+ } else {
+ hours.setSelectedIndex(value.getHours());
+ }
+ if (getResolution() >= VDateField.RESOLUTION_MIN) {
+ mins.setSelectedIndex(value.getMinutes());
+ }
+ if (getResolution() >= VDateField.RESOLUTION_SEC) {
+ sec.setSelectedIndex(value.getSeconds());
+ }
+ if (getDateTimeService().isTwelveHourClock()) {
+ ampm.setSelectedIndex(value.getHours() < 12 ? 0 : 1);
+ }
+
+ hours.setEnabled(isEnabled());
+ if (mins != null) {
+ mins.setEnabled(isEnabled());
+ }
+ if (sec != null) {
+ sec.setEnabled(isEnabled());
+ }
+ if (ampm != null) {
+ ampm.setEnabled(isEnabled());
+ }
+
+ }
+
+ private int getMilliseconds() {
+ return DateTimeService.getMilliseconds(value);
+ }
+
+ private DateTimeService getDateTimeService() {
+ if (dateTimeService == null) {
+ dateTimeService = new DateTimeService();
+ }
+ return dateTimeService;
+ }
+
+ /*
+ * (non-Javadoc) VT
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ChangeHandler#onChange(com.google.gwt
+ * .event.dom.client.ChangeEvent)
+ */
+ @Override
+ public void onChange(ChangeEvent event) {
+ /*
+ * Value from dropdowns gets always set for the value. Like year and
+ * month when resolution is month or year.
+ */
+ if (event.getSource() == hours) {
+ int h = hours.getSelectedIndex();
+ if (getDateTimeService().isTwelveHourClock()) {
+ h = h + ampm.getSelectedIndex() * 12;
+ }
+ value.setHours(h);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(h, value.getMinutes(),
+ value.getSeconds(),
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getSource() == mins) {
+ final int m = mins.getSelectedIndex();
+ value.setMinutes(m);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(value.getHours(), m,
+ value.getSeconds(),
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getSource() == sec) {
+ final int s = sec.getSelectedIndex();
+ value.setSeconds(s);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(value.getHours(),
+ value.getMinutes(), s,
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.getSource() == ampm) {
+ final int h = hours.getSelectedIndex()
+ + (ampm.getSelectedIndex() * 12);
+ value.setHours(h);
+ if (timeChangeListener != null) {
+ timeChangeListener.changed(h, value.getMinutes(),
+ value.getSeconds(),
+ DateTimeService.getMilliseconds(value));
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ }
+
+ /**
+ * A widget representing a single day in the calendar panel.
+ */
+ private class Day extends InlineHTML {
+ private static final String BASECLASS = VDateField.CLASSNAME
+ + "-calendarpanel-day";
+ private final Date date;
+
+ Day(Date date) {
+ super("" + date.getDate());
+ setStyleName(BASECLASS);
+ this.date = date;
+ addClickHandler(dayClickHandler);
+ }
+
+ public Date getDate() {
+ return date;
+ }
+ }
+
+ public Date getDate() {
+ return value;
+ }
+
+ /**
+ * If true should be returned if the panel will not be used after this
+ * event.
+ *
+ * @param event
+ * @return
+ */
+ protected boolean onTabOut(DomEvent<?> event) {
+ if (focusOutListener != null) {
+ return focusOutListener.onFocusOut(event);
+ }
+ return false;
+ }
+
+ /**
+ * A focus out listener is triggered when the panel loosed focus. This can
+ * happen either after a user clicks outside the panel or tabs out.
+ *
+ * @param listener
+ * The listener to trigger
+ */
+ public void setFocusOutListener(FocusOutListener listener) {
+ focusOutListener = listener;
+ }
+
+ /**
+ * The submit listener is called when the user selects a value from the
+ * calender either by clicking the day or selects it by keyboard.
+ *
+ * @param submitListener
+ * The listener to trigger
+ */
+ public void setSubmitListener(SubmitListener submitListener) {
+ this.submitListener = submitListener;
+ }
+
+ /**
+ * The given FocusChangeListener is notified when the focused date changes
+ * by user either clicking on a new date or by using the keyboard.
+ *
+ * @param listener
+ * The FocusChangeListener to be notified
+ */
+ public void setFocusChangeListener(FocusChangeListener listener) {
+ focusChangeListener = listener;
+ }
+
+ /**
+ * The time change listener is triggered when the user changes the time.
+ *
+ * @param listener
+ */
+ public void setTimeChangeListener(TimeChangeListener listener) {
+ timeChangeListener = listener;
+ }
+
+ /**
+ * Returns the submit listener that listens to selection made from the panel
+ *
+ * @return The listener or NULL if no listener has been set
+ */
+ public SubmitListener getSubmitListener() {
+ return submitListener;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+ @Override
+ public void onBlur(final BlurEvent event) {
+ if (event.getSource() instanceof VCalendarPanel) {
+ hasFocus = false;
+ focusDay(null);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ @Override
+ public void onFocus(FocusEvent event) {
+ if (event.getSource() instanceof VCalendarPanel) {
+ hasFocus = true;
+
+ // Focuses the current day if the calendar shows the days
+ if (focusedDay != null) {
+ focusDay(focusedDate);
+ }
+ }
+ }
+
+ private static final String SUBPART_NEXT_MONTH = "nextmon";
+ private static final String SUBPART_PREV_MONTH = "prevmon";
+
+ private static final String SUBPART_NEXT_YEAR = "nexty";
+ private static final String SUBPART_PREV_YEAR = "prevy";
+ private static final String SUBPART_HOUR_SELECT = "h";
+ private static final String SUBPART_MINUTE_SELECT = "m";
+ private static final String SUBPART_SECS_SELECT = "s";
+ private static final String SUBPART_MSECS_SELECT = "ms";
+ private static final String SUBPART_AMPM_SELECT = "ampm";
+ private static final String SUBPART_DAY = "day";
+ private static final String SUBPART_MONTH_YEAR_HEADER = "header";
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (contains(nextMonth, subElement)) {
+ return SUBPART_NEXT_MONTH;
+ } else if (contains(prevMonth, subElement)) {
+ return SUBPART_PREV_MONTH;
+ } else if (contains(nextYear, subElement)) {
+ return SUBPART_NEXT_YEAR;
+ } else if (contains(prevYear, subElement)) {
+ return SUBPART_PREV_YEAR;
+ } else if (contains(days, subElement)) {
+ // Day, find out which dayOfMonth and use that as the identifier
+ Day day = Util.findWidget(subElement, Day.class);
+ if (day != null) {
+ Date date = day.getDate();
+ int id = date.getDate();
+ // Zero or negative ids map to days of the preceding month,
+ // past-the-end-of-month ids to days of the following month
+ if (date.getMonth() < displayedMonth.getMonth()) {
+ id -= DateTimeService.getNumberOfDaysInMonth(date);
+ } else if (date.getMonth() > displayedMonth.getMonth()) {
+ id += DateTimeService
+ .getNumberOfDaysInMonth(displayedMonth);
+ }
+ return SUBPART_DAY + id;
+ }
+ } else if (time != null) {
+ if (contains(time.hours, subElement)) {
+ return SUBPART_HOUR_SELECT;
+ } else if (contains(time.mins, subElement)) {
+ return SUBPART_MINUTE_SELECT;
+ } else if (contains(time.sec, subElement)) {
+ return SUBPART_SECS_SELECT;
+ } else if (contains(time.ampm, subElement)) {
+ return SUBPART_AMPM_SELECT;
+
+ }
+ } else if (getCellFormatter().getElement(0, 2).isOrHasChild(subElement)) {
+ return SUBPART_MONTH_YEAR_HEADER;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if subElement is inside the widget DOM hierarchy.
+ *
+ * @param w
+ * @param subElement
+ * @return true if {@code w} is a parent of subElement, false otherwise.
+ */
+ private boolean contains(Widget w, Element subElement) {
+ if (w == null || w.getElement() == null) {
+ return false;
+ }
+
+ return w.getElement().isOrHasChild(subElement);
+ }
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (SUBPART_NEXT_MONTH.equals(subPart)) {
+ return nextMonth.getElement();
+ }
+ if (SUBPART_PREV_MONTH.equals(subPart)) {
+ return prevMonth.getElement();
+ }
+ if (SUBPART_NEXT_YEAR.equals(subPart)) {
+ return nextYear.getElement();
+ }
+ if (SUBPART_PREV_YEAR.equals(subPart)) {
+ return prevYear.getElement();
+ }
+ if (SUBPART_HOUR_SELECT.equals(subPart)) {
+ return time.hours.getElement();
+ }
+ if (SUBPART_MINUTE_SELECT.equals(subPart)) {
+ return time.mins.getElement();
+ }
+ if (SUBPART_SECS_SELECT.equals(subPart)) {
+ return time.sec.getElement();
+ }
+ if (SUBPART_AMPM_SELECT.equals(subPart)) {
+ return time.ampm.getElement();
+ }
+ if (subPart.startsWith(SUBPART_DAY)) {
+ // Zero or negative ids map to days in the preceding month,
+ // past-the-end-of-month ids to days in the following month
+ int dayOfMonth = Integer.parseInt(subPart.substring(SUBPART_DAY
+ .length()));
+ Date date = new Date(displayedMonth.getYear(),
+ displayedMonth.getMonth(), dayOfMonth);
+ Iterator<Widget> iter = days.iterator();
+ while (iter.hasNext()) {
+ Widget w = iter.next();
+ if (w instanceof Day) {
+ Day day = (Day) w;
+ if (day.getDate().equals(date)) {
+ return day.getElement();
+ }
+ }
+ }
+ }
+
+ if (SUBPART_MONTH_YEAR_HEADER.equals(subPart)) {
+ return (Element) getCellFormatter().getElement(0, 2).getChild(0);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (mouseTimer != null) {
+ mouseTimer.cancel();
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java
new file mode 100644
index 0000000000..614c4febdd
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateField.java
@@ -0,0 +1,185 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.ui.Field;
+
+public class VDateField extends FlowPanel implements Field {
+
+ public static final String CLASSNAME = "v-datefield";
+
+ protected String paintableId;
+
+ protected ApplicationConnection client;
+
+ protected boolean immediate;
+
+ public static final int RESOLUTION_YEAR = 1;
+ public static final int RESOLUTION_MONTH = 2;
+ public static final int RESOLUTION_DAY = 4;
+ public static final int RESOLUTION_HOUR = 8;
+ public static final int RESOLUTION_MIN = 16;
+ public static final int RESOLUTION_SEC = 32;
+
+ public static final String WEEK_NUMBERS = "wn";
+
+ static String resolutionToString(int res) {
+ if (res > RESOLUTION_DAY) {
+ return "full";
+ }
+ if (res == RESOLUTION_DAY) {
+ return "day";
+ }
+ if (res == RESOLUTION_MONTH) {
+ return "month";
+ }
+ return "year";
+ }
+
+ protected int currentResolution = RESOLUTION_YEAR;
+
+ protected String currentLocale;
+
+ protected boolean readonly;
+
+ protected boolean enabled;
+
+ /**
+ * The date that is selected in the date field. Null if an invalid date is
+ * specified.
+ */
+ private Date date = null;
+
+ protected DateTimeService dts;
+
+ protected boolean showISOWeekNumbers = false;
+
+ public VDateField() {
+ setStyleName(CLASSNAME);
+ dts = new DateTimeService();
+ }
+
+ /*
+ * We need this redundant native function because Java's Date object doesn't
+ * have a setMilliseconds method.
+ */
+ protected 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 DateTimeService.getMilliseconds(date);
+ }
+
+ public void setMilliseconds(int ms) {
+ DateTimeService.setMilliseconds(date, 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 boolean isImmediate() {
+ return immediate;
+ }
+
+ public boolean isReadonly() {
+ return readonly;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public DateTimeService getDateTimeService() {
+ return dts;
+ }
+
+ public String getId() {
+ return paintableId;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ /**
+ * Returns whether ISO 8601 week numbers should be shown in the date
+ * selector or not. ISO 8601 defines that a week always starts with a Monday
+ * so the week numbers are only shown if this is the case.
+ *
+ * @return true if week number should be shown, false otherwise
+ */
+ public boolean isShowISOWeekNumbers() {
+ return showISOWeekNumbers;
+ }
+
+ /**
+ * Returns a copy of the current date. Modifying the returned date will not
+ * modify the value of this VDateField. Use {@link #setDate(Date)} to change
+ * the current date.
+ *
+ * @return A copy of the current date
+ */
+ protected Date getDate() {
+ Date current = getCurrentDate();
+ if (current == null) {
+ return null;
+ } else {
+ return (Date) getCurrentDate().clone();
+ }
+ }
+
+ /**
+ * Sets the current date for this VDateField.
+ *
+ * @param date
+ * The new date to use
+ */
+ protected void setDate(Date date) {
+ this.date = date;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java
new file mode 100644
index 0000000000..e411fb3013
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VDateFieldCalendar.java
@@ -0,0 +1,97 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.google.gwt.event.dom.client.DomEvent;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusOutListener;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.SubmitListener;
+
+/**
+ * A client side implementation for InlineDateField
+ */
+public class VDateFieldCalendar extends VDateField {
+
+ protected final VCalendarPanel calendarPanel;
+
+ public VDateFieldCalendar() {
+ super();
+ calendarPanel = new VCalendarPanel();
+ add(calendarPanel);
+ calendarPanel.setSubmitListener(new SubmitListener() {
+ @Override
+ public void onSubmit() {
+ updateValueFromPanel();
+ }
+
+ @Override
+ public void onCancel() {
+ // TODO Auto-generated method stub
+
+ }
+ });
+ calendarPanel.setFocusOutListener(new FocusOutListener() {
+ @Override
+ public boolean onFocusOut(DomEvent<?> event) {
+ updateValueFromPanel();
+ return false;
+ }
+ });
+ }
+
+ /**
+ * TODO refactor: almost same method as in VPopupCalendar.updateValue
+ */
+ @SuppressWarnings("deprecation")
+ protected void updateValueFromPanel() {
+
+ // If field is invisible at the beginning, client can still be null when
+ // this function is called.
+ if (getClient() == null) {
+ return;
+ }
+
+ Date date2 = calendarPanel.getDate();
+ Date currentDate = getCurrentDate();
+ if (currentDate == null || date2.getTime() != currentDate.getTime()) {
+ setCurrentDate((Date) date2.clone());
+ getClient().updateVariable(getId(), "year", date2.getYear() + 1900,
+ false);
+ if (getCurrentResolution() > VDateField.RESOLUTION_YEAR) {
+ getClient().updateVariable(getId(), "month",
+ date2.getMonth() + 1, false);
+ if (getCurrentResolution() > RESOLUTION_MONTH) {
+ getClient().updateVariable(getId(), "day", date2.getDate(),
+ false);
+ if (getCurrentResolution() > RESOLUTION_DAY) {
+ getClient().updateVariable(getId(), "hour",
+ date2.getHours(), false);
+ if (getCurrentResolution() > RESOLUTION_HOUR) {
+ getClient().updateVariable(getId(), "min",
+ date2.getMinutes(), false);
+ if (getCurrentResolution() > RESOLUTION_MIN) {
+ getClient().updateVariable(getId(), "sec",
+ date2.getSeconds(), false);
+ if (getCurrentResolution() > RESOLUTION_SEC) {
+ getClient().updateVariable(
+ getId(),
+ "msec",
+ DateTimeService
+ .getMilliseconds(date2),
+ false);
+ }
+ }
+ }
+ }
+ }
+ }
+ if (isImmediate()) {
+ getClient().sendPendingVariableChanges();
+ }
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java
new file mode 100644
index 0000000000..de6ebf29af
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VPopupCalendar.java
@@ -0,0 +1,373 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.Button;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.FocusOutListener;
+import com.vaadin.terminal.gwt.client.ui.datefield.VCalendarPanel.SubmitListener;
+
+/**
+ * Represents a date selection component with a text field and a popup date
+ * selector.
+ *
+ * <b>Note:</b> To change the keyboard assignments used in the popup dialog you
+ * should extend <code>com.vaadin.terminal.gwt.client.ui.VCalendarPanel</code>
+ * and then pass set it by calling the
+ * <code>setCalendarPanel(VCalendarPanel panel)</code> method.
+ *
+ */
+public class VPopupCalendar extends VTextualDate implements Field,
+ ClickHandler, CloseHandler<PopupPanel>, SubPartAware {
+
+ protected static final String POPUP_PRIMARY_STYLE_NAME = VDateField.CLASSNAME
+ + "-popup";
+
+ protected final Button calendarToggle;
+
+ protected VCalendarPanel calendar;
+
+ protected final VOverlay popup;
+ private boolean open = false;
+ protected boolean parsable = true;
+
+ public VPopupCalendar() {
+ super();
+
+ calendarToggle = new Button();
+ calendarToggle.setStyleName(CLASSNAME + "-button");
+ calendarToggle.setText("");
+ calendarToggle.addClickHandler(this);
+ // -2 instead of -1 to avoid FocusWidget.onAttach to reset it
+ calendarToggle.getElement().setTabIndex(-2);
+ add(calendarToggle);
+
+ calendar = GWT.create(VCalendarPanel.class);
+ calendar.setFocusOutListener(new FocusOutListener() {
+ @Override
+ public boolean onFocusOut(DomEvent<?> event) {
+ event.preventDefault();
+ closeCalendarPanel();
+ return true;
+ }
+ });
+
+ calendar.setSubmitListener(new SubmitListener() {
+ @Override
+ public void onSubmit() {
+ // Update internal value and send valuechange event if immediate
+ updateValue(calendar.getDate());
+
+ // Update text field (a must when not immediate).
+ buildDate(true);
+
+ closeCalendarPanel();
+ }
+
+ @Override
+ public void onCancel() {
+ closeCalendarPanel();
+ }
+ });
+
+ popup = new VOverlay(true, true, true);
+ popup.setStyleName(POPUP_PRIMARY_STYLE_NAME);
+ popup.setWidget(calendar);
+ popup.addCloseHandler(this);
+
+ DOM.setElementProperty(calendar.getElement(), "id",
+ "PID_VAADIN_POPUPCAL");
+
+ sinkEvents(Event.ONKEYDOWN);
+
+ }
+
+ @SuppressWarnings("deprecation")
+ protected void updateValue(Date newDate) {
+ Date currentDate = getCurrentDate();
+ if (currentDate == null || newDate.getTime() != currentDate.getTime()) {
+ setCurrentDate((Date) newDate.clone());
+ getClient().updateVariable(getId(), "year",
+ newDate.getYear() + 1900, false);
+ if (getCurrentResolution() > VDateField.RESOLUTION_YEAR) {
+ getClient().updateVariable(getId(), "month",
+ newDate.getMonth() + 1, false);
+ if (getCurrentResolution() > RESOLUTION_MONTH) {
+ getClient().updateVariable(getId(), "day",
+ newDate.getDate(), false);
+ if (getCurrentResolution() > RESOLUTION_DAY) {
+ getClient().updateVariable(getId(), "hour",
+ newDate.getHours(), false);
+ if (getCurrentResolution() > RESOLUTION_HOUR) {
+ getClient().updateVariable(getId(), "min",
+ newDate.getMinutes(), false);
+ if (getCurrentResolution() > RESOLUTION_MIN) {
+ getClient().updateVariable(getId(), "sec",
+ newDate.getSeconds(), false);
+ }
+ }
+ }
+ }
+ }
+ if (isImmediate()) {
+ getClient().sendPendingVariableChanges();
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String)
+ */
+ @Override
+ public void setStyleName(String style) {
+ // make sure the style is there before size calculation
+ super.setStyleName(style + " " + CLASSNAME + "-popupcalendar");
+ }
+
+ /**
+ * Opens the calendar panel popup
+ */
+ public void openCalendarPanel() {
+
+ if (!open && !readonly) {
+ open = true;
+
+ if (getCurrentDate() != null) {
+ calendar.setDate((Date) getCurrentDate().clone());
+ } else {
+ calendar.setDate(new Date());
+ }
+
+ // clear previous values
+ popup.setWidth("");
+ popup.setHeight("");
+ popup.setPopupPositionAndShow(new PositionCallback() {
+ @Override
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ final int w = offsetWidth;
+ final int h = offsetHeight;
+ final int browserWindowWidth = Window.getClientWidth()
+ + Window.getScrollLeft();
+ final int browserWindowHeight = Window.getClientHeight()
+ + Window.getScrollTop();
+ int t = calendarToggle.getAbsoluteTop();
+ int l = calendarToggle.getAbsoluteLeft();
+
+ // Add a little extra space to the right to avoid
+ // problems with IE7 scrollbars and to make it look
+ // nicer.
+ int extraSpace = 30;
+
+ boolean overflowRight = false;
+ if (l + +w + extraSpace > browserWindowWidth) {
+ overflowRight = true;
+ // Part of the popup is outside the browser window
+ // (to the right)
+ l = browserWindowWidth - w - extraSpace;
+ }
+
+ if (t + h + calendarToggle.getOffsetHeight() + 30 > browserWindowHeight) {
+ // Part of the popup is outside the browser window
+ // (below)
+ t = browserWindowHeight - h
+ - calendarToggle.getOffsetHeight() - 30;
+ if (!overflowRight) {
+ // Show to the right of the popup button unless we
+ // are in the lower right corner of the screen
+ l += calendarToggle.getOffsetWidth();
+ }
+ }
+
+ // fix size
+ popup.setWidth(w + "px");
+ popup.setHeight(h + "px");
+
+ popup.setPopupPosition(l,
+ t + calendarToggle.getOffsetHeight() + 2);
+
+ /*
+ * We have to wait a while before focusing since the popup
+ * needs to be opened before we can focus
+ */
+ Timer focusTimer = new Timer() {
+ @Override
+ public void run() {
+ setFocus(true);
+ }
+ };
+
+ focusTimer.schedule(100);
+ }
+ });
+ } else {
+ VConsole.error("Cannot reopen popup, it is already open!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event
+ * .dom.client.ClickEvent)
+ */
+ @Override
+ public void onClick(ClickEvent event) {
+ if (event.getSource() == calendarToggle && isEnabled()) {
+ openCalendarPanel();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt
+ * .event.logical.shared.CloseEvent)
+ */
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (event.getSource() == popup) {
+ buildDate();
+ if (!BrowserInfo.get().isTouchDevice()) {
+ /*
+ * Move focus to textbox, unless on touch device (avoids opening
+ * virtual keyboard).
+ */
+ focus();
+ }
+
+ // TODO resolve what the "Sigh." is all about and document it here
+ // 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);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ui.VTextualDate#buildDate()
+ */
+ @Override
+ protected void buildDate() {
+ // Save previous value
+ String previousValue = getText();
+ super.buildDate();
+
+ // Restore previous value if the input could not be parsed
+ if (!parsable) {
+ setText(previousValue);
+ }
+ }
+
+ /**
+ * Update the text field contents from the date. See {@link #buildDate()}.
+ *
+ * @param forceValid
+ * true to force the text field to be updated, false to only
+ * update if the parsable flag is true.
+ */
+ protected void buildDate(boolean forceValid) {
+ if (forceValid) {
+ parsable = true;
+ }
+ buildDate();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.VDateField#onBrowserEvent(com.google
+ * .gwt.user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(com.google.gwt.user.client.Event event) {
+ super.onBrowserEvent(event);
+ if (DOM.eventGetType(event) == Event.ONKEYDOWN
+ && event.getKeyCode() == getOpenCalenderPanelKey()) {
+ openCalendarPanel();
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Get the key code that opens the calendar panel. By default it is the down
+ * key but you can override this to be whatever you like
+ *
+ * @return
+ */
+ protected int getOpenCalenderPanelKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Closes the open popup panel
+ */
+ public void closeCalendarPanel() {
+ if (open) {
+ popup.hide(true);
+ }
+ }
+
+ private final String CALENDAR_TOGGLE_ID = "popupButton";
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (subPart.equals(CALENDAR_TOGGLE_ID)) {
+ return calendarToggle.getElement();
+ }
+
+ return super.getSubPartElement(subPart);
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (calendarToggle.getElement().isOrHasChild(subElement)) {
+ return CALENDAR_TOGGLE_ID;
+ }
+
+ return super.getSubPartName(subElement);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java
new file mode 100644
index 0000000000..8c252ddc69
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/datefield/VTextualDate.java
@@ -0,0 +1,340 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.datefield;
+
+import java.util.Date;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.TextBox;
+import com.vaadin.shared.EventId;
+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.VConsole;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+public class VTextualDate extends VDateField implements Field, ChangeHandler,
+ Focusable, SubPartAware {
+
+ private static final String PARSE_ERROR_CLASSNAME = CLASSNAME
+ + "-parseerror";
+
+ protected final TextBox text;
+
+ protected String formatStr;
+
+ protected boolean lenient;
+
+ private static final String CLASSNAME_PROMPT = "prompt";
+ protected static final String ATTR_INPUTPROMPT = "prompt";
+ protected String inputPrompt = "";
+ private boolean prompting = false;
+
+ public VTextualDate() {
+
+ super();
+ text = new TextBox();
+ // use normal textfield styles as a basis
+ text.setStyleName(VTextField.CLASSNAME);
+ // add datefield spesific style name also
+ text.addStyleName(CLASSNAME + "-textfield");
+ text.addChangeHandler(this);
+ text.addFocusHandler(new FocusHandler() {
+ @Override
+ public void onFocus(FocusEvent event) {
+ text.addStyleName(VTextField.CLASSNAME + "-"
+ + VTextField.CLASSNAME_FOCUS);
+ if (prompting) {
+ text.setText("");
+ setPrompting(false);
+ }
+ if (getClient() != null
+ && getClient().hasEventListeners(VTextualDate.this,
+ EventId.FOCUS)) {
+ getClient()
+ .updateVariable(getId(), EventId.FOCUS, "", true);
+ }
+ }
+ });
+ text.addBlurHandler(new BlurHandler() {
+ @Override
+ public void onBlur(BlurEvent event) {
+ text.removeStyleName(VTextField.CLASSNAME + "-"
+ + VTextField.CLASSNAME_FOCUS);
+ String value = getText();
+ setPrompting(inputPrompt != null
+ && (value == null || "".equals(value)));
+ if (prompting) {
+ text.setText(readonly ? "" : inputPrompt);
+ }
+ if (getClient() != null
+ && getClient().hasEventListeners(VTextualDate.this,
+ EventId.BLUR)) {
+ getClient().updateVariable(getId(), EventId.BLUR, "", true);
+ }
+ }
+ });
+ add(text);
+ }
+
+ 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 (dts.isTwelveHourClock()) {
+ frmString += " aaa";
+ }
+
+ }
+
+ formatStr = frmString;
+ } catch (LocaleNotLoadedException e) {
+ // TODO should die instead? Can the component survive
+ // without format string?
+ VConsole.error(e);
+ }
+ }
+ }
+ return formatStr;
+ }
+
+ /**
+ * Updates the text field according to the current date (provided by
+ * {@link #getDate()}). Takes care of updating text, enabling and disabling
+ * the field, setting/removing readonly status and updating readonly styles.
+ *
+ * TODO: Split part of this into a method that only updates the text as this
+ * is what usually is needed except for updateFromUIDL.
+ */
+ protected void buildDate() {
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ // Create the initial text for the textfield
+ String dateText;
+ Date currentDate = getDate();
+ if (currentDate != null) {
+ dateText = getDateTimeService().formatDate(currentDate,
+ getFormatString());
+ } else {
+ dateText = "";
+ }
+
+ setText(dateText);
+ text.setEnabled(enabled);
+ text.setReadOnly(readonly);
+
+ if (readonly) {
+ text.addStyleName("v-readonly");
+ } else {
+ text.removeStyleName("v-readonly");
+ }
+
+ }
+
+ protected void setPrompting(boolean prompting) {
+ this.prompting = prompting;
+ if (prompting) {
+ addStyleDependentName(CLASSNAME_PROMPT);
+ } else {
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onChange(ChangeEvent event) {
+ if (!text.getText().equals("")) {
+ try {
+ String enteredDate = text.getText();
+
+ setDate(getDateTimeService().parseDate(enteredDate,
+ getFormatString(), lenient));
+
+ if (lenient) {
+ // If date value was leniently parsed, normalize text
+ // presentation.
+ // FIXME: Add a description/example here of when this is
+ // needed
+ text.setValue(
+ getDateTimeService().formatDate(getDate(),
+ getFormatString()), false);
+ }
+
+ // remove possibly added invalid value indication
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ } catch (final Exception e) {
+ VConsole.log(e);
+
+ addStyleName(PARSE_ERROR_CLASSNAME);
+ // this is a hack that may eventually be removed
+ getClient().updateVariable(getId(), "lastInvalidDateString",
+ text.getText(), false);
+ setDate(null);
+ }
+ } else {
+ setDate(null);
+ // remove possibly added invalid value indication
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ }
+ // always send the date string
+ getClient()
+ .updateVariable(getId(), "dateString", text.getText(), false);
+
+ // Update variables
+ // (only the smallest defining resolution needs to be
+ // immediate)
+ Date currentDate = getDate();
+ getClient().updateVariable(getId(), "year",
+ currentDate != null ? currentDate.getYear() + 1900 : -1,
+ currentResolution == VDateField.RESOLUTION_YEAR && immediate);
+ if (currentResolution >= VDateField.RESOLUTION_MONTH) {
+ getClient().updateVariable(
+ getId(),
+ "month",
+ currentDate != null ? currentDate.getMonth() + 1 : -1,
+ currentResolution == VDateField.RESOLUTION_MONTH
+ && immediate);
+ }
+ if (currentResolution >= VDateField.RESOLUTION_DAY) {
+ getClient()
+ .updateVariable(
+ getId(),
+ "day",
+ currentDate != null ? currentDate.getDate() : -1,
+ currentResolution == VDateField.RESOLUTION_DAY
+ && immediate);
+ }
+ if (currentResolution >= VDateField.RESOLUTION_HOUR) {
+ getClient().updateVariable(
+ getId(),
+ "hour",
+ currentDate != null ? currentDate.getHours() : -1,
+ currentResolution == VDateField.RESOLUTION_HOUR
+ && immediate);
+ }
+ if (currentResolution >= VDateField.RESOLUTION_MIN) {
+ getClient()
+ .updateVariable(
+ getId(),
+ "min",
+ currentDate != null ? currentDate.getMinutes() : -1,
+ currentResolution == VDateField.RESOLUTION_MIN
+ && immediate);
+ }
+ if (currentResolution >= VDateField.RESOLUTION_SEC) {
+ getClient()
+ .updateVariable(
+ getId(),
+ "sec",
+ currentDate != null ? currentDate.getSeconds() : -1,
+ currentResolution == VDateField.RESOLUTION_SEC
+ && immediate);
+ }
+
+ }
+
+ private String cleanFormat(String format) {
+ // Remove unnecessary d & M if resolution is too low
+ if (currentResolution < VDateField.RESOLUTION_DAY) {
+ format = format.replaceAll("d", "");
+ }
+ if (currentResolution < VDateField.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 focus() {
+ text.setFocus(true);
+ }
+
+ protected String getText() {
+ if (prompting) {
+ return "";
+ }
+ return text.getText();
+ }
+
+ protected void setText(String text) {
+ if (inputPrompt != null && (text == null || "".equals(text))) {
+ text = readonly ? "" : inputPrompt;
+ setPrompting(true);
+ } else {
+ setPrompting(false);
+ }
+
+ this.text.setText(text);
+ }
+
+ private final String TEXTFIELD_ID = "field";
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (subPart.equals(TEXTFIELD_ID)) {
+ return text.getElement();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (text.getElement().isOrHasChild(subElement)) {
+ return TEXTFIELD_ID;
+ }
+
+ return null;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java
new file mode 100644
index 0000000000..f9ec8a2f48
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/DDUtil.java
@@ -0,0 +1,100 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Window;
+import com.vaadin.shared.ui.dd.HorizontalDropLocation;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class DDUtil {
+
+ /**
+ * @deprecated use the version with the actual event instead of detected
+ * clientY value
+ *
+ * @param element
+ * @param clientY
+ * @param topBottomRatio
+ * @return
+ */
+ @Deprecated
+ public static VerticalDropLocation getVerticalDropLocation(Element element,
+ int clientY, double topBottomRatio) {
+ int offsetHeight = element.getOffsetHeight();
+ return getVerticalDropLocation(element, offsetHeight, clientY,
+ topBottomRatio);
+ }
+
+ public static VerticalDropLocation getVerticalDropLocation(Element element,
+ NativeEvent event, double topBottomRatio) {
+ int offsetHeight = element.getOffsetHeight();
+ return getVerticalDropLocation(element, offsetHeight, event,
+ topBottomRatio);
+ }
+
+ public static VerticalDropLocation getVerticalDropLocation(Element element,
+ int offsetHeight, NativeEvent event, double topBottomRatio) {
+ int clientY = Util.getTouchOrMouseClientY(event);
+ return getVerticalDropLocation(element, offsetHeight, clientY,
+ topBottomRatio);
+ }
+
+ public static VerticalDropLocation getVerticalDropLocation(Element element,
+ int offsetHeight, int clientY, double topBottomRatio) {
+
+ // Event coordinates are relative to the viewport, element absolute
+ // position is relative to the document. Make element position relative
+ // to viewport by adjusting for viewport scrolling. See #6021
+ int elementTop = element.getAbsoluteTop() - Window.getScrollTop();
+ int fromTop = clientY - elementTop;
+
+ float percentageFromTop = (fromTop / (float) offsetHeight);
+ if (percentageFromTop < topBottomRatio) {
+ return VerticalDropLocation.TOP;
+ } else if (percentageFromTop > 1 - topBottomRatio) {
+ return VerticalDropLocation.BOTTOM;
+ } else {
+ return VerticalDropLocation.MIDDLE;
+ }
+ }
+
+ public static HorizontalDropLocation getHorizontalDropLocation(
+ Element element, NativeEvent event, double leftRightRatio) {
+ int touchOrMouseClientX = Util.getTouchOrMouseClientX(event);
+ return getHorizontalDropLocation(element, touchOrMouseClientX,
+ leftRightRatio);
+ }
+
+ /**
+ * @deprecated use the version with the actual event
+ * @param element
+ * @param clientX
+ * @param leftRightRatio
+ * @return
+ */
+ @Deprecated
+ public static HorizontalDropLocation getHorizontalDropLocation(
+ Element element, int clientX, double leftRightRatio) {
+
+ // Event coordinates are relative to the viewport, element absolute
+ // position is relative to the document. Make element position relative
+ // to viewport by adjusting for viewport scrolling. See #6021
+ int elementLeft = element.getAbsoluteLeft() - Window.getScrollLeft();
+ int offsetWidth = element.getOffsetWidth();
+ int fromLeft = clientX - elementLeft;
+
+ float percentageFromLeft = (fromLeft / (float) offsetWidth);
+ if (percentageFromLeft < leftRightRatio) {
+ return HorizontalDropLocation.LEFT;
+ } else if (percentageFromLeft > 1 - leftRightRatio) {
+ return HorizontalDropLocation.RIGHT;
+ } else {
+ return HorizontalDropLocation.CENTER;
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java
new file mode 100644
index 0000000000..ce0533ac1f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAbstractDropHandler.java
@@ -0,0 +1,142 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import java.util.Iterator;
+
+import com.google.gwt.user.client.Command;
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public abstract class VAbstractDropHandler implements VDropHandler {
+
+ private UIDL criterioUIDL;
+ private VAcceptCriterion acceptCriteria = new VAcceptAll();
+
+ /**
+ * Implementor/user of {@link VAbstractDropHandler} must pass the UIDL
+ * painted by {@link AcceptCriterion} to this method. Practically the
+ * details about {@link AcceptCriterion} are saved.
+ *
+ * @param uidl
+ */
+ public void updateAcceptRules(UIDL uidl) {
+ criterioUIDL = uidl;
+ /*
+ * supports updating the accept rule root directly or so that it is
+ * contained in given uidl node
+ */
+ if (!uidl.getTag().equals("-ac")) {
+ Iterator<Object> childIterator = uidl.getChildIterator();
+ while (!uidl.getTag().equals("-ac") && childIterator.hasNext()) {
+ uidl = (UIDL) childIterator.next();
+ }
+ }
+ acceptCriteria = VAcceptCriteria.get(uidl.getStringAttribute("name"));
+ if (acceptCriteria == null) {
+ throw new IllegalArgumentException(
+ "No accept criteria found with given name "
+ + uidl.getStringAttribute("name"));
+ }
+ }
+
+ /**
+ * Default implementation does nothing.
+ */
+ @Override
+ public void dragOver(VDragEvent drag) {
+
+ }
+
+ /**
+ * Default implementation does nothing. Implementors should clean possible
+ * emphasis or drag icons here.
+ */
+ @Override
+ public void dragLeave(VDragEvent drag) {
+
+ }
+
+ /**
+ * The default implementation in {@link VAbstractDropHandler} checks if the
+ * Transferable is accepted.
+ * <p>
+ * If transferable is accepted (either via server visit or client side
+ * rules) the default implementation calls abstract
+ * {@link #dragAccepted(VDragEvent)} method.
+ * <p>
+ * If drop handler has distinct places where some parts may accept the
+ * {@link Transferable} and others don't, one should use similar validation
+ * logic in dragOver method and replace this method with empty
+ * implementation.
+ *
+ */
+ @Override
+ public void dragEnter(final VDragEvent drag) {
+ validate(new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ dragAccepted(drag);
+ }
+ }, drag);
+ }
+
+ /**
+ * This method is called when a valid drop location was found with
+ * {@link AcceptCriterion} either via client or server side check.
+ * <p>
+ * Implementations can set some hints for users here to highlight that the
+ * drag is on a valid drop location.
+ *
+ * @param drag
+ */
+ abstract protected void dragAccepted(VDragEvent drag);
+
+ protected void validate(final VAcceptCallback cb, final VDragEvent event) {
+ Command checkCriteria = new Command() {
+ @Override
+ public void execute() {
+ acceptCriteria.accept(event, criterioUIDL, cb);
+ }
+ };
+
+ VDragAndDropManager.get().executeWhenReady(checkCriteria);
+ }
+
+ boolean validated = false;
+
+ /**
+ * The default implemmentation visits server if {@link AcceptCriterion}
+ * can't be verified on client or if {@link AcceptCriterion} are met on
+ * client.
+ */
+ @Override
+ public boolean drop(VDragEvent drag) {
+ if (acceptCriteria.needsServerSideCheck(drag, criterioUIDL)) {
+ return true;
+ } else {
+ validated = false;
+ acceptCriteria.accept(drag, criterioUIDL, new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ validated = true;
+ }
+ });
+ return validated;
+ }
+
+ }
+
+ /**
+ * Returns the Paintable who owns this {@link VAbstractDropHandler}. Server
+ * side counterpart of the Paintable is expected to implement
+ * {@link DropTarget} interface.
+ */
+ @Override
+ public abstract ComponentConnector getConnector();
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java
new file mode 100644
index 0000000000..7ce0d69727
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptAll.java
@@ -0,0 +1,20 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.AcceptAll;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+@AcceptCriterion(AcceptAll.class)
+final public class VAcceptAll extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ return true;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java
new file mode 100644
index 0000000000..cd9ade88c2
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCallback.java
@@ -0,0 +1,17 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+public interface VAcceptCallback {
+
+ /**
+ * This method is called by {@link VDragAndDropManager} if the
+ * {@link VDragEvent} is still active. Developer can update for example drag
+ * icon or empahsis the target if the target accepts the transferable. If
+ * the drag and drop operation ends or the {@link VAbstractDropHandler} has
+ * changed before response arrives, the method is never called.
+ */
+ public void accepted(VDragEvent event);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java
new file mode 100644
index 0000000000..2deed95915
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriteria.java
@@ -0,0 +1,22 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.google.gwt.core.client.GWT;
+
+/**
+ * A class via all AcceptCriteria instances are fetched by an identifier.
+ */
+public class VAcceptCriteria {
+ private static VAcceptCriterionFactory impl;
+
+ static {
+ impl = GWT.create(VAcceptCriterionFactory.class);
+ }
+
+ public static VAcceptCriterion get(String name) {
+ return impl.get(name);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java
new file mode 100644
index 0000000000..b1a88166ec
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterion.java
@@ -0,0 +1,45 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public abstract class VAcceptCriterion {
+
+ /**
+ * Checks if current drag event has valid drop target and target accepts the
+ * transferable. If drop target is valid, callback is used.
+ *
+ * @param drag
+ * @param configuration
+ * @param callback
+ */
+ public void accept(final VDragEvent drag, UIDL configuration,
+ final VAcceptCallback callback) {
+ if (needsServerSideCheck(drag, configuration)) {
+ VDragEventServerCallback acceptCallback = new VDragEventServerCallback() {
+ @Override
+ public void handleResponse(boolean accepted, UIDL response) {
+ if (accepted) {
+ callback.accepted(drag);
+ }
+ }
+ };
+ VDragAndDropManager.get().visitServer(acceptCallback);
+ } else {
+ boolean validates = accept(drag, configuration);
+ if (validates) {
+ callback.accepted(drag);
+ }
+ }
+
+ }
+
+ protected abstract boolean accept(VDragEvent drag, UIDL configuration);
+
+ public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) {
+ return false;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java
new file mode 100644
index 0000000000..fe627665c5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAcceptCriterionFactory.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.terminal.gwt.widgetsetutils.AcceptCriteriaFactoryGenerator;
+
+/**
+ * Generated by {@link AcceptCriteriaFactoryGenerator}
+ *
+ */
+public abstract class VAcceptCriterionFactory {
+
+ public abstract VAcceptCriterion get(String name);
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java
new file mode 100644
index 0000000000..2f0cdc2a80
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VAnd.java
@@ -0,0 +1,42 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.And;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+@AcceptCriterion(And.class)
+final public class VAnd extends VAcceptCriterion implements VAcceptCallback {
+ private boolean b1;
+
+ static VAcceptCriterion getCriteria(VDragEvent drag, UIDL configuration,
+ int i) {
+ UIDL childUIDL = configuration.getChildUIDL(i);
+ return VAcceptCriteria.get(childUIDL.getStringAttribute("name"));
+ }
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ int childCount = configuration.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ VAcceptCriterion crit = getCriteria(drag, configuration, i);
+ b1 = false;
+ crit.accept(drag, configuration.getChildUIDL(i), this);
+ if (!b1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void accepted(VDragEvent event) {
+ b1 = true;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java
new file mode 100644
index 0000000000..7c5d9f769a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VContainsDataFlavor.java
@@ -0,0 +1,21 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+@AcceptCriterion(ContainsDataFlavor.class)
+final public class VContainsDataFlavor extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ String name = configuration.getStringAttribute("p");
+ return drag.getTransferable().getDataFlavors().contains(name);
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java
new file mode 100644
index 0000000000..bc98860716
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragAndDropManager.java
@@ -0,0 +1,738 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.DragEventType;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ValueMap;
+
+/**
+ * Helper class to manage the state of drag and drop event on Vaadin client
+ * side. Can be used to implement most of the drag and drop operation
+ * automatically via cross-browser event preview method or just as a helper when
+ * implementing own low level drag and drop operation (like with HTML5 api).
+ * <p>
+ * Singleton. Only one drag and drop operation can be active anyways. Use
+ * {@link #get()} to get instance.
+ *
+ * TODO cancel drag and drop if more than one touches !?
+ */
+public class VDragAndDropManager {
+
+ public static final String ACTIVE_DRAG_SOURCE_STYLENAME = "v-active-drag-source";
+
+ private final class DefaultDragAndDropEventHandler implements
+ NativePreviewHandler {
+
+ @Override
+ public void onPreviewNativeEvent(NativePreviewEvent event) {
+ NativeEvent nativeEvent = event.getNativeEvent();
+
+ int typeInt = event.getTypeInt();
+ if (typeInt == Event.ONKEYDOWN) {
+ int keyCode = event.getNativeEvent().getKeyCode();
+ if (keyCode == KeyCodes.KEY_ESCAPE) {
+ // end drag if ESC is hit
+ interruptDrag();
+ event.cancel();
+ event.getNativeEvent().preventDefault();
+ }
+ // no use for handling for any key down event
+ return;
+ }
+
+ currentDrag.setCurrentGwtEvent(nativeEvent);
+ updateDragImagePosition();
+
+ Element targetElement = Element.as(nativeEvent.getEventTarget());
+ if (Util.isTouchEvent(nativeEvent)
+ || (dragElement != null && dragElement
+ .isOrHasChild(targetElement))) {
+ // to detect the "real" target, hide dragelement temporary and
+ // use elementFromPoint
+ String display = dragElement.getStyle().getDisplay();
+ dragElement.getStyle().setDisplay(Display.NONE);
+ try {
+ int x = Util.getTouchOrMouseClientX(nativeEvent);
+ int y = Util.getTouchOrMouseClientY(nativeEvent);
+ // Util.browserDebugger();
+ targetElement = Util.getElementFromPoint(x, y);
+ if (targetElement == null) {
+ // ApplicationConnection.getConsole().log(
+ // "Event on dragImage, ignored");
+ event.cancel();
+ nativeEvent.stopPropagation();
+ return;
+
+ } else {
+ // ApplicationConnection.getConsole().log(
+ // "Event on dragImage, target changed");
+ // special handling for events over dragImage
+ // pretty much all events are mousemove althout below
+ // kind of happens mouseover
+ switch (typeInt) {
+ case Event.ONMOUSEOVER:
+ case Event.ONMOUSEOUT:
+ // ApplicationConnection
+ // .getConsole()
+ // .log(
+ // "IGNORING proxy image event, fired because of hack or not significant");
+ return;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ VDropHandler findDragTarget = findDragTarget(targetElement);
+ if (findDragTarget != currentDropHandler) {
+ // dragleave on old
+ if (currentDropHandler != null) {
+ currentDropHandler.dragLeave(currentDrag);
+ currentDrag.getDropDetails().clear();
+ serverCallback = null;
+ }
+ // dragenter on new
+ currentDropHandler = findDragTarget;
+ if (findDragTarget != null) {
+ // ApplicationConnection.getConsole().log(
+ // "DropHandler now"
+ // + currentDropHandler
+ // .getPaintable());
+ }
+
+ if (currentDropHandler != null) {
+ currentDrag
+ .setElementOver((com.google.gwt.user.client.Element) targetElement);
+ currentDropHandler.dragEnter(currentDrag);
+ }
+ } else if (findDragTarget != null) {
+ currentDrag
+ .setElementOver((com.google.gwt.user.client.Element) targetElement);
+ currentDropHandler.dragOver(currentDrag);
+ }
+ // prevent text selection on IE
+ nativeEvent.preventDefault();
+ return;
+ default:
+ // just update element over and let the actual
+ // handling code do the thing
+ // ApplicationConnection.getConsole().log(
+ // "Target just modified on "
+ // + event.getType());
+ currentDrag
+ .setElementOver((com.google.gwt.user.client.Element) targetElement);
+ break;
+ }
+
+ }
+ } catch (RuntimeException e) {
+ // ApplicationConnection.getConsole().log(
+ // "ERROR during elementFromPoint hack.");
+ throw e;
+ } finally {
+ dragElement.getStyle().setProperty("display", display);
+ }
+ }
+
+ switch (typeInt) {
+ case Event.ONMOUSEOVER:
+ VDropHandler target = findDragTarget(targetElement);
+
+ if (target != null && target != currentDropHandler) {
+ if (currentDropHandler != null) {
+ currentDropHandler.dragLeave(currentDrag);
+ currentDrag.getDropDetails().clear();
+ }
+
+ currentDropHandler = target;
+ // ApplicationConnection.getConsole().log(
+ // "DropHandler now"
+ // + currentDropHandler.getPaintable());
+ currentDrag
+ .setElementOver((com.google.gwt.user.client.Element) targetElement);
+ target.dragEnter(currentDrag);
+ } else if (target == null && currentDropHandler != null) {
+ // ApplicationConnection.getConsole().log("Invalid state!?");
+ currentDropHandler.dragLeave(currentDrag);
+ currentDrag.getDropDetails().clear();
+ currentDropHandler = null;
+ }
+ break;
+ case Event.ONMOUSEOUT:
+ Element relatedTarget = Element.as(nativeEvent
+ .getRelatedEventTarget());
+ VDropHandler newDragHanler = findDragTarget(relatedTarget);
+ if (dragElement != null
+ && dragElement.isOrHasChild(relatedTarget)) {
+ // ApplicationConnection.getConsole().log(
+ // "Mouse out of dragImage, ignored");
+ return;
+ }
+
+ if (currentDropHandler != null
+ && currentDropHandler != newDragHanler) {
+ currentDropHandler.dragLeave(currentDrag);
+ currentDrag.getDropDetails().clear();
+ currentDropHandler = null;
+ serverCallback = null;
+ }
+ break;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ if (currentDropHandler != null) {
+ currentDrag
+ .setElementOver((com.google.gwt.user.client.Element) targetElement);
+ currentDropHandler.dragOver(currentDrag);
+ }
+ nativeEvent.preventDefault();
+
+ break;
+
+ case Event.ONTOUCHEND:
+ /* Avoid simulated event on drag end */
+ event.getNativeEvent().preventDefault();
+ case Event.ONMOUSEUP:
+ endDrag();
+ break;
+
+ default:
+ break;
+ }
+
+ }
+
+ }
+
+ public static final String DD_SERVICE = "DD";
+
+ private static VDragAndDropManager instance;
+ private HandlerRegistration handlerRegistration;
+ private VDragEvent currentDrag;
+
+ /**
+ * If dragging is currently on a drophandler, this field has reference to it
+ */
+ private VDropHandler currentDropHandler;
+
+ public VDropHandler getCurrentDropHandler() {
+ return currentDropHandler;
+ }
+
+ /**
+ * If drag and drop operation is not handled by {@link VDragAndDropManager}s
+ * internal handler, this can be used to update current {@link VDropHandler}
+ * .
+ *
+ * @param currentDropHandler
+ */
+ public void setCurrentDropHandler(VDropHandler currentDropHandler) {
+ this.currentDropHandler = currentDropHandler;
+ }
+
+ private VDragEventServerCallback serverCallback;
+
+ private HandlerRegistration deferredStartRegistration;
+
+ public static VDragAndDropManager get() {
+ if (instance == null) {
+ instance = GWT.create(VDragAndDropManager.class);
+ }
+ return instance;
+ }
+
+ /* Singleton */
+ private VDragAndDropManager() {
+ }
+
+ private NativePreviewHandler defaultDragAndDropEventHandler = new DefaultDragAndDropEventHandler();
+
+ /**
+ * Flag to indicate if drag operation has really started or not. Null check
+ * of currentDrag field is not enough as a lazy start may be pending.
+ */
+ private boolean isStarted;
+
+ /**
+ * This method is used to start Vaadin client side drag and drop operation.
+ * Operation may be started by virtually any Widget.
+ * <p>
+ * Cancels possible existing drag. TODO figure out if this is always a bug
+ * if one is active. Maybe a good and cheap lifesaver thought.
+ * <p>
+ * If possible, method automatically detects current {@link VDropHandler}
+ * and fires {@link VDropHandler#dragEnter(VDragEvent)} event on it.
+ * <p>
+ * May also be used to control the drag and drop operation. If this option
+ * is used, {@link VDropHandler} is searched on mouse events and appropriate
+ * methods on it called automatically.
+ *
+ * @param transferable
+ * @param nativeEvent
+ * @param handleDragEvents
+ * if true, {@link VDragAndDropManager} handles the drag and drop
+ * operation GWT event preview.
+ * @return
+ */
+ public VDragEvent startDrag(VTransferable transferable,
+ final NativeEvent startEvent, final boolean handleDragEvents) {
+ interruptDrag();
+ isStarted = false;
+
+ currentDrag = new VDragEvent(transferable, startEvent);
+ currentDrag.setCurrentGwtEvent(startEvent);
+
+ final Command startDrag = new Command() {
+
+ @Override
+ public void execute() {
+ isStarted = true;
+ addActiveDragSourceStyleName();
+ VDropHandler dh = null;
+ if (startEvent != null) {
+ dh = findDragTarget(Element.as(currentDrag
+ .getCurrentGwtEvent().getEventTarget()));
+ }
+ if (dh != null) {
+ // drag has started on a DropHandler, kind of drag over
+ // happens
+ currentDropHandler = dh;
+ dh.dragEnter(currentDrag);
+ }
+
+ if (handleDragEvents) {
+ handlerRegistration = Event
+ .addNativePreviewHandler(defaultDragAndDropEventHandler);
+ if (dragElement != null
+ && dragElement.getParentElement() == null) {
+ // deferred attaching drag image is on going, we can
+ // hurry with it now
+ lazyAttachDragElement.cancel();
+ lazyAttachDragElement.run();
+ }
+ }
+ // just capture something to prevent text selection in IE
+ Event.setCapture(RootPanel.getBodyElement());
+ }
+
+ private void addActiveDragSourceStyleName() {
+ ComponentConnector dragSource = currentDrag.getTransferable()
+ .getDragSource();
+ dragSource.getWidget().addStyleName(
+ ACTIVE_DRAG_SOURCE_STYLENAME);
+ }
+ };
+
+ final int eventType = Event.as(startEvent).getTypeInt();
+ if (handleDragEvents
+ && (eventType == Event.ONMOUSEDOWN || eventType == Event.ONTOUCHSTART)) {
+ // only really start drag event on mousemove
+ deferredStartRegistration = Event
+ .addNativePreviewHandler(new NativePreviewHandler() {
+
+ @Override
+ public void onPreviewNativeEvent(
+ NativePreviewEvent event) {
+ int typeInt = event.getTypeInt();
+ switch (typeInt) {
+ case Event.ONMOUSEOVER:
+ if (dragElement == null) {
+ break;
+ }
+ EventTarget currentEventTarget = event
+ .getNativeEvent()
+ .getCurrentEventTarget();
+ if (Node.is(currentEventTarget)
+ && !dragElement.isOrHasChild(Node
+ .as(currentEventTarget))) {
+ // drag image appeared below, ignore
+ break;
+ }
+ case Event.ONKEYDOWN:
+ case Event.ONKEYPRESS:
+ case Event.ONKEYUP:
+ case Event.ONBLUR:
+ case Event.ONFOCUS:
+ // don't cancel possible drag start
+ break;
+ case Event.ONMOUSEOUT:
+
+ if (dragElement == null) {
+ break;
+ }
+ EventTarget relatedEventTarget = event
+ .getNativeEvent()
+ .getRelatedEventTarget();
+ if (Node.is(relatedEventTarget)
+ && !dragElement.isOrHasChild(Node
+ .as(relatedEventTarget))) {
+ // drag image appeared below, ignore
+ break;
+ }
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ if (deferredStartRegistration != null) {
+ deferredStartRegistration.removeHandler();
+ deferredStartRegistration = null;
+ }
+ currentDrag.setCurrentGwtEvent(event
+ .getNativeEvent());
+ startDrag.execute();
+ break;
+ default:
+ // on any other events, clean up the
+ // deferred drag start
+ if (deferredStartRegistration != null) {
+ deferredStartRegistration.removeHandler();
+ deferredStartRegistration = null;
+ }
+ currentDrag = null;
+ clearDragElement();
+ break;
+ }
+ }
+
+ });
+
+ } else {
+ startDrag.execute();
+ }
+
+ return currentDrag;
+ }
+
+ private void updateDragImagePosition() {
+ if (currentDrag.getCurrentGwtEvent() != null && dragElement != null) {
+ Style style = dragElement.getStyle();
+ int clientY = Util.getTouchOrMouseClientY(currentDrag
+ .getCurrentGwtEvent());
+ int clientX = Util.getTouchOrMouseClientX(currentDrag
+ .getCurrentGwtEvent());
+ style.setTop(clientY, Unit.PX);
+ style.setLeft(clientX, Unit.PX);
+ }
+ }
+
+ /**
+ * First seeks the widget from this element, then iterates widgets until one
+ * implement HasDropHandler. Returns DropHandler from that.
+ *
+ * @param element
+ * @return
+ */
+ private VDropHandler findDragTarget(Element element) {
+ try {
+ Widget w = Util.findWidget(
+ (com.google.gwt.user.client.Element) element, null);
+ if (w == null) {
+ return null;
+ }
+ while (!(w instanceof VHasDropHandler)) {
+ w = w.getParent();
+ if (w == null) {
+ break;
+ }
+ }
+ if (w == null) {
+ return null;
+ } else {
+ VDropHandler dh = ((VHasDropHandler) w).getDropHandler();
+ return dh;
+ }
+
+ } catch (Exception e) {
+ // ApplicationConnection.getConsole().log(
+ // "FIXME: Exception when detecting drop handler");
+ // e.printStackTrace();
+ return null;
+ }
+
+ }
+
+ /**
+ * Drag is ended (drop happened) on current drop handler. Calls drop method
+ * on current drop handler and does appropriate cleanup.
+ */
+ public void endDrag() {
+ endDrag(true);
+ }
+
+ /**
+ * The drag and drop operation is ended, but drop did not happen. If
+ * operation is currently on a drop handler, its dragLeave method is called
+ * and appropriate cleanup happens.
+ */
+ public void interruptDrag() {
+ endDrag(false);
+ }
+
+ private void endDrag(boolean doDrop) {
+ if (handlerRegistration != null) {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+ boolean sendTransferableToServer = false;
+ if (currentDropHandler != null) {
+ if (doDrop) {
+ // we have dropped on a drop target
+ sendTransferableToServer = currentDropHandler.drop(currentDrag);
+ if (sendTransferableToServer) {
+ doRequest(DragEventType.DROP);
+ /*
+ * Clean active source class name deferred until response is
+ * handled. E.g. hidden on start, removed in drophandler ->
+ * would flicker in case removed eagerly.
+ */
+ final ComponentConnector dragSource = currentDrag
+ .getTransferable().getDragSource();
+ final ApplicationConnection client = currentDropHandler
+ .getApplicationConnection();
+ Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+ @Override
+ public boolean execute() {
+ if (!client.hasActiveRequest()) {
+ removeActiveDragSourceStyleName(dragSource);
+ return false;
+ }
+ return true;
+ }
+
+ }, 30);
+
+ }
+ } else {
+ currentDrag.setCurrentGwtEvent(null);
+ currentDropHandler.dragLeave(currentDrag);
+ }
+ currentDropHandler = null;
+ serverCallback = null;
+ visitId = 0; // reset to ignore ongoing server check
+ }
+
+ /*
+ * Remove class name indicating drag source when server visit is done
+ * iff server visit was not initiated. Otherwise it will be removed once
+ * the server visit is done.
+ */
+ if (!sendTransferableToServer && currentDrag != null) {
+ removeActiveDragSourceStyleName(currentDrag.getTransferable()
+ .getDragSource());
+ }
+
+ currentDrag = null;
+
+ clearDragElement();
+
+ // release the capture (set to prevent text selection in IE)
+ Event.releaseCapture(RootPanel.getBodyElement());
+
+ }
+
+ private void removeActiveDragSourceStyleName(ComponentConnector dragSource) {
+ dragSource.getWidget().removeStyleName(ACTIVE_DRAG_SOURCE_STYLENAME);
+ }
+
+ private void clearDragElement() {
+ if (dragElement != null) {
+ if (dragElement.getParentElement() != null) {
+ RootPanel.getBodyElement().removeChild(dragElement);
+ }
+ dragElement = null;
+ }
+ }
+
+ private int visitId = 0;
+ private Element dragElement;
+
+ /**
+ * Visits server during drag and drop procedure. Transferable and event type
+ * is given to server side counterpart of DropHandler.
+ *
+ * If another server visit is started before the current is received, the
+ * current is just dropped. TODO consider if callback should have
+ * interrupted() method for cleanup.
+ *
+ * @param acceptCallback
+ */
+ public void visitServer(VDragEventServerCallback acceptCallback) {
+ doRequest(DragEventType.ENTER);
+ serverCallback = acceptCallback;
+ }
+
+ private void doRequest(DragEventType drop) {
+ if (currentDropHandler == null) {
+ return;
+ }
+ ComponentConnector paintable = currentDropHandler.getConnector();
+ ApplicationConnection client = currentDropHandler
+ .getApplicationConnection();
+ /*
+ * For drag events we are using special id that are routed to
+ * "drag service" which then again finds the corresponding DropHandler
+ * on server side.
+ *
+ * TODO add rest of the data in Transferable
+ *
+ * TODO implement partial updates to Transferable (currently the whole
+ * Transferable is sent on each request)
+ */
+ visitId++;
+ client.updateVariable(DD_SERVICE, "visitId", visitId, false);
+ client.updateVariable(DD_SERVICE, "eventId", currentDrag.getEventId(),
+ false);
+ client.updateVariable(DD_SERVICE, "dhowner", paintable, false);
+
+ VTransferable transferable = currentDrag.getTransferable();
+
+ client.updateVariable(DD_SERVICE, "component",
+ transferable.getDragSource(), false);
+
+ client.updateVariable(DD_SERVICE, "type", drop.ordinal(), false);
+
+ if (currentDrag.getCurrentGwtEvent() != null) {
+ try {
+ MouseEventDetails mouseEventDetails = MouseEventDetailsBuilder
+ .buildMouseEventDetails(currentDrag
+ .getCurrentGwtEvent());
+ currentDrag.getDropDetails().put("mouseEvent",
+ mouseEventDetails.serialize());
+ } catch (Exception e) {
+ // NOP, (at least oophm on Safari) can't serialize html dd event
+ // to mouseevent
+ }
+ } else {
+ currentDrag.getDropDetails().put("mouseEvent", null);
+ }
+ client.updateVariable(DD_SERVICE, "evt", currentDrag.getDropDetails(),
+ false);
+
+ client.updateVariable(DD_SERVICE, "tra", transferable.getVariableMap(),
+ true);
+
+ }
+
+ public void handleServerResponse(ValueMap valueMap) {
+ if (serverCallback == null) {
+ return;
+ }
+ UIDL uidl = (UIDL) valueMap.cast();
+ int visitId = uidl.getIntAttribute("visitId");
+
+ if (this.visitId == visitId) {
+ serverCallback.handleResponse(uidl.getBooleanAttribute("accepted"),
+ uidl);
+ serverCallback = null;
+ }
+ runDeferredCommands();
+ }
+
+ private void runDeferredCommands() {
+ if (deferredCommand != null) {
+ Command command = deferredCommand;
+ deferredCommand = null;
+ command.execute();
+ if (!isBusy()) {
+ runDeferredCommands();
+ }
+ }
+ }
+
+ void setDragElement(Element node) {
+ if (currentDrag != null) {
+ if (dragElement != null && dragElement != node) {
+ clearDragElement();
+ } else if (node == dragElement) {
+ return;
+ }
+
+ dragElement = node;
+ dragElement.addClassName("v-drag-element");
+ updateDragImagePosition();
+
+ if (isStarted) {
+ lazyAttachDragElement.run();
+ } else {
+ /*
+ * To make our default dnd handler as compatible as possible, we
+ * need to defer the appearance of dragElement. Otherwise events
+ * that are derived from sequences of other events might not
+ * fire as domchanged will fire between them or mouse up might
+ * happen on dragElement.
+ */
+ lazyAttachDragElement.schedule(300);
+ }
+ }
+ }
+
+ Element getDragElement() {
+ return dragElement;
+ }
+
+ private final Timer lazyAttachDragElement = new Timer() {
+
+ @Override
+ public void run() {
+ if (dragElement != null && dragElement.getParentElement() == null) {
+ RootPanel.getBodyElement().appendChild(dragElement);
+ }
+
+ }
+ };
+
+ private Command deferredCommand;
+
+ private boolean isBusy() {
+ return serverCallback != null;
+ }
+
+ /**
+ * Method to que tasks until all dd related server visits are done
+ *
+ * @param command
+ */
+ private void defer(Command command) {
+ deferredCommand = command;
+ }
+
+ /**
+ * Method to execute commands when all existing dd related tasks are
+ * completed (some may require server visit).
+ * <p>
+ * Using this method may be handy if criterion that uses lazy initialization
+ * are used. Check
+ * <p>
+ * TODO Optimization: consider if we actually only need to keep the last
+ * command in queue here.
+ *
+ * @param command
+ */
+ public void executeWhenReady(Command command) {
+ if (isBusy()) {
+ defer(command);
+ } else {
+ command.execute();
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java
new file mode 100644
index 0000000000..cece2720d0
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEvent.java
@@ -0,0 +1,189 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.user.client.Element;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ * DragEvent used by Vaadin client side engine. Supports components, items,
+ * properties and custom payload (HTML5 style).
+ *
+ *
+ */
+public class VDragEvent {
+
+ private static final int DEFAULT_OFFSET = 10;
+
+ private static int eventId = 0;
+
+ private VTransferable transferable;
+
+ private NativeEvent currentGwtEvent;
+
+ private NativeEvent startEvent;
+
+ private int id;
+
+ private HashMap<String, Object> dropDetails = new HashMap<String, Object>();
+
+ private Element elementOver;
+
+ VDragEvent(VTransferable t, NativeEvent startEvent) {
+ transferable = t;
+ this.startEvent = startEvent;
+ id = eventId++;
+ }
+
+ public VTransferable getTransferable() {
+ return transferable;
+ }
+
+ /**
+ * Returns the the latest {@link NativeEvent} that relates to this drag and
+ * drop operation. For example on {@link VDropHandler#dragEnter(VDragEvent)}
+ * this is commonly a {@link MouseOverEvent}.
+ *
+ * @return
+ */
+ public NativeEvent getCurrentGwtEvent() {
+ return currentGwtEvent;
+ }
+
+ public void setCurrentGwtEvent(NativeEvent event) {
+ currentGwtEvent = event;
+ }
+
+ int getEventId() {
+ return id;
+ }
+
+ /**
+ * Detecting the element on which the the event is happening may be
+ * problematic during drag and drop operation. This is especially the case
+ * if a drag image (often called also drag proxy) is kept under the mouse
+ * cursor (see {@link #createDragImage(Element, boolean)}. Drag and drop
+ * event handlers (like the one provided by {@link VDragAndDropManager} )
+ * should set elmentOver field to reflect the the actual element on which
+ * the pointer currently is (drag image excluded). {@link VDropHandler}s can
+ * then more easily react properly on drag events by reading the element via
+ * this method.
+ *
+ * @return the element in {@link VDropHandler} on which mouse cursor is on
+ */
+ public Element getElementOver() {
+ if (elementOver != null) {
+ return elementOver;
+ } else if (currentGwtEvent != null) {
+ return currentGwtEvent.getEventTarget().cast();
+ }
+ return null;
+ }
+
+ public void setElementOver(Element targetElement) {
+ elementOver = targetElement;
+ }
+
+ /**
+ * Sets the drag image used for current drag and drop operation. Drag image
+ * is displayed next to mouse cursor during drag and drop.
+ * <p>
+ * The element to be used as drag image will automatically get CSS style
+ * name "v-drag-element".
+ *
+ * TODO decide if this method should be here or in {@link VTransferable} (in
+ * HTML5 it is in DataTransfer) or {@link VDragAndDropManager}
+ *
+ * TODO should be possible to override behavior. Like to proxy the element
+ * to HTML5 DataTransfer
+ *
+ * @param node
+ */
+ public void setDragImage(Element node) {
+ setDragImage(node, DEFAULT_OFFSET, DEFAULT_OFFSET);
+ }
+
+ /**
+ * TODO consider using similar smaller (than map) api as in Transferable
+ *
+ * TODO clean up when drop handler changes
+ *
+ * @return
+ */
+ public Map<String, Object> getDropDetails() {
+ return dropDetails;
+ }
+
+ /**
+ * Sets the drag image used for current drag and drop operation. Drag image
+ * is displayed next to mouse cursor during drag and drop.
+ * <p>
+ * The element to be used as drag image will automatically get CSS style
+ * name "v-drag-element".
+ *
+ * @param element
+ * the dom element to be positioned next to mouse cursor
+ * @param offsetX
+ * the horizontal offset of drag image from mouse cursor
+ * @param offsetY
+ * the vertical offset of drag image from mouse cursor
+ */
+ public void setDragImage(Element element, int offsetX, int offsetY) {
+ element.getStyle().setMarginLeft(offsetX, Unit.PX);
+ element.getStyle().setMarginTop(offsetY, Unit.PX);
+ VDragAndDropManager.get().setDragElement(element);
+ }
+
+ /**
+ * @return the current Element used as a drag image (aka drag proxy) or null
+ * if drag image is not currently set for this drag operation.
+ */
+ public Element getDragImage() {
+ return (Element) VDragAndDropManager.get().getDragElement();
+ }
+
+ /**
+ * Automatically tries to create a proxy image from given element.
+ *
+ * @param element
+ * @param alignImageToEvent
+ * if true, proxy image is aligned to start event, else next to
+ * mouse cursor
+ */
+ public void createDragImage(Element element, boolean alignImageToEvent) {
+ Element cloneNode = (Element) element.cloneNode(true);
+ if (BrowserInfo.get().isIE()) {
+ if (cloneNode.getTagName().toLowerCase().equals("tr")) {
+ TableElement table = Document.get().createTableElement();
+ TableSectionElement tbody = Document.get().createTBodyElement();
+ table.appendChild(tbody);
+ tbody.appendChild(cloneNode);
+ cloneNode = table.cast();
+ }
+ }
+ if (alignImageToEvent) {
+ int absoluteTop = element.getAbsoluteTop();
+ int absoluteLeft = element.getAbsoluteLeft();
+ int clientX = Util.getTouchOrMouseClientX(startEvent);
+ int clientY = Util.getTouchOrMouseClientY(startEvent);
+ int offsetX = absoluteLeft - clientX;
+ int offsetY = absoluteTop - clientY;
+ setDragImage(cloneNode, offsetX, offsetY);
+ } else {
+ setDragImage(cloneNode);
+ }
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java
new file mode 100644
index 0000000000..e2547dbf52
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragEventServerCallback.java
@@ -0,0 +1,12 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public interface VDragEventServerCallback {
+
+ public void handleResponse(boolean accepted, UIDL response);
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java
new file mode 100644
index 0000000000..ffb923f3e0
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDragSourceIs.java
@@ -0,0 +1,42 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.SourceIs;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+/**
+ * TODO Javadoc!
+ *
+ * @since 6.3
+ */
+@AcceptCriterion(SourceIs.class)
+final public class VDragSourceIs extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ try {
+ ComponentConnector component = drag.getTransferable()
+ .getDragSource();
+ int c = configuration.getIntAttribute("c");
+ for (int i = 0; i < c; i++) {
+ String requiredPid = configuration
+ .getStringAttribute("component" + i);
+ VDropHandler currentDropHandler = VDragAndDropManager.get()
+ .getCurrentDropHandler();
+ ComponentConnector paintable = (ComponentConnector) ConnectorMap
+ .get(currentDropHandler.getApplicationConnection())
+ .getConnector(requiredPid);
+ if (paintable == component) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java
new file mode 100644
index 0000000000..92bd6abe61
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VDropHandler.java
@@ -0,0 +1,73 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+
+/**
+ * Vaadin Widgets that want to receive something via drag and drop implement
+ * this interface.
+ */
+public interface VDropHandler {
+
+ /**
+ * Called by DragAndDropManager when a drag operation is in progress and the
+ * cursor enters the area occupied by this Paintable.
+ *
+ * @param dragEvent
+ * DragEvent which contains the transferable and other
+ * information for the operation
+ */
+ public void dragEnter(VDragEvent dragEvent);
+
+ /**
+ * Called by DragAndDropManager when a drag operation is in progress and the
+ * cursor leaves the area occupied by this Paintable.
+ *
+ * @param dragEvent
+ * DragEvent which contains the transferable and other
+ * information for the operation
+ */
+ public void dragLeave(VDragEvent dragEvent);
+
+ /**
+ * Called by DragAndDropManager when a drag operation was in progress and a
+ * drop was performed on this Paintable.
+ *
+ *
+ * @param dragEvent
+ * DragEvent which contains the transferable and other
+ * information for the operation
+ *
+ * @return true if the Tranferrable of this drag event needs to be sent to
+ * the server, false if drop is rejected or no server side event
+ * should be sent
+ */
+ public boolean drop(VDragEvent drag);
+
+ /**
+ * When drag is over current drag handler.
+ *
+ * With drag implementation by {@link VDragAndDropManager} will be called
+ * when mouse is moved. HTML5 implementations call this continuously even
+ * though mouse is not moved.
+ *
+ * @param currentDrag
+ */
+ public void dragOver(VDragEvent currentDrag);
+
+ /**
+ * Returns the ComponentConnector with which this DropHandler is associated
+ */
+ public ComponentConnector getConnector();
+
+ /**
+ * Returns the application connection to which this {@link VDropHandler}
+ * belongs to. DragAndDropManager uses this fucction to send Transferable to
+ * server side.
+ */
+ public ApplicationConnection getApplicationConnection();
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java
new file mode 100644
index 0000000000..6d6f7c776d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHasDropHandler.java
@@ -0,0 +1,17 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+
+/**
+ * Used to detect Widget from widget tree that has {@link #getDropHandler()}
+ *
+ * Decide whether to get rid of this class. If so, {@link VAbstractDropHandler}
+ * must extend {@link ComponentConnector}.
+ *
+ */
+public interface VHasDropHandler {
+ public VDropHandler getDropHandler();
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java
new file mode 100644
index 0000000000..7bdb291e44
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent.java
@@ -0,0 +1,84 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.NativeEvent;
+
+/**
+ * Helper class to access html5 style drag events.
+ *
+ * TODO Gears support ?
+ */
+public class VHtml5DragEvent extends NativeEvent {
+ protected VHtml5DragEvent() {
+ }
+
+ public final native JsArrayString getTypes()
+ /*-{
+ // IE does not support types, return some basic values
+ return this.dataTransfer.types ? this.dataTransfer.types : ["Text","Url","Html"];
+ }-*/;
+
+ public final native String getDataAsText(String type)
+ /*-{
+ var v = this.dataTransfer.getData(type);
+ return v;
+ }-*/;
+
+ /**
+ * Works on FF 3.6 and possibly with gears.
+ *
+ * @param index
+ * @return
+ */
+ public final native String getFileAsString(int index)
+ /*-{
+ if(this.dataTransfer.files.length > 0 && this.dataTransfer.files[0].getAsText) {
+ return this.dataTransfer.files[index].getAsText("UTF-8");
+ }
+ return null;
+ }-*/;
+
+ /**
+ * @deprecated As of Vaadin 6.8, replaced by {@link #setDropEffect(String)}.
+ */
+ @Deprecated
+ public final void setDragEffect(String effect) {
+ setDropEffect(effect);
+ }
+
+ public final native void setDropEffect(String effect)
+ /*-{
+ try {
+ this.dataTransfer.dropEffect = effect;
+ } catch (e){}
+ }-*/;
+
+ public final native String getEffectAllowed()
+ /*-{
+ return this.dataTransfer.effectAllowed;
+ }-*/;
+
+ public final native void setEffectAllowed(String effect)
+ /*-{
+ this.dataTransfer.effectAllowed = effect;
+ }-*/;
+
+ public final native int getFileCount()
+ /*-{
+ return this.dataTransfer.files ? this.dataTransfer.files.length : 0;
+ }-*/;
+
+ public final native VHtml5File getFile(int fileIndex)
+ /*-{
+ return this.dataTransfer.files[fileIndex];
+ }-*/;
+
+ public final native void setHtml5DataFlavor(String flavor, String data)
+ /*-{
+ this.dataTransfer.setData(flavor, data);
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java
new file mode 100644
index 0000000000..434ae732c1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VHtml5File.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Wrapper for html5 File object.
+ */
+public class VHtml5File extends JavaScriptObject {
+
+ protected VHtml5File() {
+ };
+
+ public native final String getName()
+ /*-{
+ return this.name;
+ }-*/;
+
+ public native final String getType()
+ /*-{
+ return this.type;
+ }-*/;
+
+ public native final int getSize()
+ /*-{
+ return this.size ? this.size : 0;
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java
new file mode 100644
index 0000000000..d0055d48dd
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VIsOverId.java
@@ -0,0 +1,45 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.ui.AbstractSelect;
+
+@AcceptCriterion(AbstractSelect.TargetItemIs.class)
+final public class VIsOverId extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ try {
+
+ String pid = configuration.getStringAttribute("s");
+ VDropHandler currentDropHandler = VDragAndDropManager.get()
+ .getCurrentDropHandler();
+ ComponentConnector dropHandlerConnector = currentDropHandler
+ .getConnector();
+ ConnectorMap paintableMap = ConnectorMap.get(currentDropHandler
+ .getApplicationConnection());
+
+ String pid2 = dropHandlerConnector.getConnectorId();
+ if (pid2.equals(pid)) {
+ Object searchedId = drag.getDropDetails().get("itemIdOver");
+ String[] stringArrayAttribute = configuration
+ .getStringArrayAttribute("keys");
+ for (String string : stringArrayAttribute) {
+ if (string.equals(searchedId)) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java
new file mode 100644
index 0000000000..67f323a950
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VItemIdIs.java
@@ -0,0 +1,40 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.ui.AbstractSelect;
+
+@AcceptCriterion(AbstractSelect.AcceptItem.class)
+final public class VItemIdIs extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ try {
+ String pid = configuration.getStringAttribute("s");
+ ComponentConnector dragSource = drag.getTransferable()
+ .getDragSource();
+ VDropHandler currentDropHandler = VDragAndDropManager.get()
+ .getCurrentDropHandler();
+ String pid2 = dragSource.getConnectorId();
+ if (pid2.equals(pid)) {
+ Object searchedId = drag.getTransferable().getData("itemId");
+ String[] stringArrayAttribute = configuration
+ .getStringArrayAttribute("keys");
+ for (String string : stringArrayAttribute) {
+ if (string.equals(searchedId)) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java
new file mode 100644
index 0000000000..bfda603d58
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VLazyInitItemIdentifiers.java
@@ -0,0 +1,81 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import java.util.HashSet;
+
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.Tree;
+
+/**
+ *
+ */
+public class VLazyInitItemIdentifiers extends VAcceptCriterion {
+ private boolean loaded = false;
+ private HashSet<String> hashSet;
+ private VDragEvent lastDragEvent;
+
+ @AcceptCriterion(Table.TableDropCriterion.class)
+ final public static class VTableLazyInitItemIdentifiers extends
+ VLazyInitItemIdentifiers {
+ // all logic in superclass
+ }
+
+ @AcceptCriterion(Tree.TreeDropCriterion.class)
+ final public static class VTreeLazyInitItemIdentifiers extends
+ VLazyInitItemIdentifiers {
+ // all logic in superclass
+ }
+
+ @Override
+ public void accept(final VDragEvent drag, UIDL configuration,
+ final VAcceptCallback callback) {
+ if (lastDragEvent == null || lastDragEvent != drag) {
+ loaded = false;
+ lastDragEvent = drag;
+ }
+ if (loaded) {
+ Object object = drag.getDropDetails().get("itemIdOver");
+ if (hashSet.contains(object)) {
+ callback.accepted(drag);
+ }
+ } else {
+
+ VDragEventServerCallback acceptCallback = new VDragEventServerCallback() {
+
+ @Override
+ public void handleResponse(boolean accepted, UIDL response) {
+ hashSet = new HashSet<String>();
+ String[] stringArrayAttribute = response
+ .getStringArrayAttribute("allowedIds");
+ for (int i = 0; i < stringArrayAttribute.length; i++) {
+ hashSet.add(stringArrayAttribute[i]);
+ }
+ loaded = true;
+ if (accepted) {
+ callback.accepted(drag);
+ }
+ }
+ };
+
+ VDragAndDropManager.get().visitServer(acceptCallback);
+ }
+
+ }
+
+ @Override
+ public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) {
+ return loaded;
+ }
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ return false; // not used is this implementation
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java
new file mode 100644
index 0000000000..8355afc625
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VNot.java
@@ -0,0 +1,64 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.Not;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VConsole;
+
+/**
+ * TODO implementation could now be simplified/optimized
+ *
+ */
+@AcceptCriterion(Not.class)
+final public class VNot extends VAcceptCriterion {
+ private boolean b1;
+ private VAcceptCriterion crit1;
+
+ @Override
+ public void accept(VDragEvent drag, UIDL configuration,
+ VAcceptCallback callback) {
+ if (crit1 == null) {
+ crit1 = getCriteria(drag, configuration, 0);
+ if (crit1 == null) {
+ VConsole.log("Not criteria didn't found a child criteria");
+ return;
+ }
+ }
+
+ b1 = false;
+
+ VAcceptCallback accept1cb = new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ b1 = true;
+ }
+ };
+
+ crit1.accept(drag, configuration.getChildUIDL(0), accept1cb);
+ if (!b1) {
+ callback.accepted(drag);
+ }
+ }
+
+ private VAcceptCriterion getCriteria(VDragEvent drag, UIDL configuration,
+ int i) {
+ UIDL childUIDL = configuration.getChildUIDL(i);
+ return VAcceptCriteria.get(childUIDL.getStringAttribute("name"));
+ }
+
+ @Override
+ public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) {
+ return false; // TODO enforce on server side
+ }
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ return false; // not used
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java
new file mode 100644
index 0000000000..46bf28b42a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOr.java
@@ -0,0 +1,50 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.Or;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+/**
+ *
+ */
+@AcceptCriterion(Or.class)
+final public class VOr extends VAcceptCriterion implements VAcceptCallback {
+ private boolean accepted;
+
+ @Override
+ public void accept(VDragEvent drag, UIDL configuration,
+ VAcceptCallback callback) {
+ int childCount = configuration.getChildCount();
+ accepted = false;
+ for (int i = 0; i < childCount; i++) {
+ VAcceptCriterion crit = VAnd.getCriteria(drag, configuration, i);
+ crit.accept(drag, configuration.getChildUIDL(i), this);
+ if (accepted == true) {
+ callback.accepted(drag);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) {
+ return false;
+ }
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ return false; // not used here
+ }
+
+ @Override
+ public void accepted(VDragEvent event) {
+ accepted = true;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java
new file mode 100644
index 0000000000..1539054c88
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VOverTreeNode.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.terminal.gwt.client.UIDL;
+
+final public class VOverTreeNode extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ Boolean containsKey = (Boolean) drag.getDropDetails().get(
+ "itemIdOverIsNode");
+ return containsKey != null && containsKey.booleanValue();
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java
new file mode 100644
index 0000000000..42d4d90ae4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VServerAccept.java
@@ -0,0 +1,39 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+@AcceptCriterion(ServerSideCriterion.class)
+final public class VServerAccept extends VAcceptCriterion {
+ @Override
+ public void accept(final VDragEvent drag, UIDL configuration,
+ final VAcceptCallback callback) {
+
+ VDragEventServerCallback acceptCallback = new VDragEventServerCallback() {
+ @Override
+ public void handleResponse(boolean accepted, UIDL response) {
+ if (accepted) {
+ callback.accepted(drag);
+ }
+ }
+ };
+ VDragAndDropManager.get().visitServer(acceptCallback);
+ }
+
+ @Override
+ public boolean needsServerSideCheck(VDragEvent drag, UIDL criterioUIDL) {
+ return true;
+ }
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ return false; // not used
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java
new file mode 100644
index 0000000000..dcb2f405fa
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VSourceIsTarget.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.SourceIsTarget;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+@AcceptCriterion(SourceIsTarget.class)
+final public class VSourceIsTarget extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ ComponentConnector dragSource = drag.getTransferable().getDragSource();
+ ComponentConnector paintable = VDragAndDropManager.get()
+ .getCurrentDropHandler().getConnector();
+
+ return paintable == dragSource;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java
new file mode 100644
index 0000000000..e67d81387c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetDetailIs.java
@@ -0,0 +1,39 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.vaadin.event.dd.acceptcriteria.TargetDetailIs;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+@AcceptCriterion(TargetDetailIs.class)
+final public class VTargetDetailIs extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ String name = configuration.getStringAttribute("p");
+ String t = configuration.hasAttribute("t") ? configuration
+ .getStringAttribute("t").intern() : "s";
+ Object value = null;
+ if (t == "s") {
+ value = configuration.getStringAttribute("v");
+ } else if (t == "b") {
+ value = configuration.getBooleanAttribute("v");
+ }
+ if (value != null) {
+ Object object = drag.getDropDetails().get(name);
+ if (object instanceof Enum) {
+ return ((Enum<?>) object).name().equals(value);
+ } else {
+ return value.equals(object);
+ }
+ } else {
+ return false;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java
new file mode 100644
index 0000000000..14bca0ed91
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTargetInSubtree.java
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.tree.VTree;
+import com.vaadin.terminal.gwt.client.ui.tree.VTree.TreeNode;
+import com.vaadin.ui.Tree;
+
+@AcceptCriterion(Tree.TargetInSubtree.class)
+final public class VTargetInSubtree extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+
+ VTree tree = (VTree) VDragAndDropManager.get().getCurrentDropHandler()
+ .getConnector();
+ TreeNode treeNode = tree.getNodeByKey((String) drag.getDropDetails()
+ .get("itemIdOver"));
+ if (treeNode != null) {
+ Widget parent2 = treeNode;
+ int depth = configuration.getIntAttribute("depth");
+ if (depth < 0) {
+ depth = Integer.MAX_VALUE;
+ }
+ final String searchedKey = configuration.getStringAttribute("key");
+ for (int i = 0; i <= depth && parent2 instanceof TreeNode; i++) {
+ if (searchedKey.equals(((TreeNode) parent2).key)) {
+ return true;
+ }
+ // panel -> next level node
+ parent2 = parent2.getParent().getParent();
+ }
+ }
+
+ return false;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java
new file mode 100644
index 0000000000..f87da378d7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/dd/VTransferable.java
@@ -0,0 +1,69 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.dd;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+
+/**
+ * Client side counterpart for Transferable in com.vaadin.event.Transferable
+ *
+ */
+public class VTransferable {
+
+ private ComponentConnector component;
+
+ private final Map<String, Object> variables = new HashMap<String, Object>();
+
+ /**
+ * Returns the component from which the transferable is created (eg. a tree
+ * which node is dragged).
+ *
+ * @return the component
+ */
+ public ComponentConnector getDragSource() {
+ return component;
+ }
+
+ /**
+ * Sets the component currently being dragged or from which the transferable
+ * is created (eg. a tree which node is dragged).
+ * <p>
+ * The server side counterpart of the component may implement
+ * {@link DragSource} interface if it wants to translate or complement the
+ * server side instance of this Transferable.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setDragSource(ComponentConnector component) {
+ this.component = component;
+ }
+
+ public Object getData(String dataFlavor) {
+ return variables.get(dataFlavor);
+ }
+
+ public void setData(String dataFlavor, Object value) {
+ variables.put(dataFlavor, value);
+ }
+
+ public Collection<String> getDataFlavors() {
+ return variables.keySet();
+ }
+
+ /**
+ * This helper method should only be called by {@link VDragAndDropManager}.
+ *
+ * @return data in this Transferable that needs to be moved to server.
+ */
+ Map<String, Object> getVariableMap() {
+ return variables;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png
new file mode 100644
index 0000000000..49b918ec0c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png b/client/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/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png
new file mode 100644
index 0000000000..7cd07369dc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png
new file mode 100644
index 0000000000..c2e1f49efe
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png
new file mode 100644
index 0000000000..417c9aecfd
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png
new file mode 100644
index 0000000000..2f1e461b0a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png
new file mode 100644
index 0000000000..63984cdee7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png
new file mode 100644
index 0000000000..1e730c072b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png
new file mode 100644
index 0000000000..34e47d1551
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png
new file mode 100644
index 0000000000..99e3709acc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png
new file mode 100644
index 0000000000..be9a4cd8c5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png
new file mode 100644
index 0000000000..0b555ad1e7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png
new file mode 100644
index 0000000000..8ff42ed0f4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java
new file mode 100644
index 0000000000..6914b451fa
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/DragAndDropWrapperConnector.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.draganddropwrapper;
+
+import java.util.HashMap;
+import java.util.Set;
+
+import com.vaadin.shared.ui.Connect;
+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.ui.customcomponent.CustomComponentConnector;
+import com.vaadin.ui.DragAndDropWrapper;
+
+@Connect(DragAndDropWrapper.class)
+public class DragAndDropWrapperConnector extends CustomComponentConnector
+ implements Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().client = client;
+ if (isRealUpdate(uidl) && !uidl.hasAttribute("hidden")) {
+ UIDL acceptCrit = uidl.getChildByTagName("-ac");
+ if (acceptCrit == null) {
+ getWidget().dropHandler = null;
+ } else {
+ if (getWidget().dropHandler == null) {
+ getWidget().dropHandler = getWidget().new CustomDropHandler();
+ }
+ getWidget().dropHandler.updateAcceptRules(acceptCrit);
+ }
+
+ Set<String> variableNames = uidl.getVariableNames();
+ for (String fileId : variableNames) {
+ if (fileId.startsWith("rec-")) {
+ String receiverUrl = uidl.getStringVariable(fileId);
+ fileId = fileId.substring(4);
+ if (getWidget().fileIdToReceiver == null) {
+ getWidget().fileIdToReceiver = new HashMap<String, String>();
+ }
+ if ("".equals(receiverUrl)) {
+ Integer id = Integer.parseInt(fileId);
+ int indexOf = getWidget().fileIds.indexOf(id);
+ if (indexOf != -1) {
+ getWidget().files.remove(indexOf);
+ getWidget().fileIds.remove(indexOf);
+ }
+ } else {
+ getWidget().fileIdToReceiver.put(fileId, receiverUrl);
+ }
+ }
+ }
+ getWidget().startNextUpload();
+
+ getWidget().dragStartMode = uidl
+ .getIntAttribute(VDragAndDropWrapper.DRAG_START_MODE);
+ getWidget().initDragStartMode();
+ getWidget().html5DataFlavors = uidl
+ .getMapAttribute(VDragAndDropWrapper.HTML5_DATA_FLAVORS);
+
+ // Used to prevent wrapper from stealing tooltips when not defined
+ getWidget().hasTooltip = getState().hasDescription();
+ }
+ }
+
+ @Override
+ public VDragAndDropWrapper getWidget() {
+ return (VDragAndDropWrapper) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java
new file mode 100644
index 0000000000..e77055764e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapper.java
@@ -0,0 +1,596 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.draganddropwrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.user.client.Command;
+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.google.gwt.xhr.client.ReadyStateChangeHandler;
+import com.google.gwt.xhr.client.XMLHttpRequest;
+import com.vaadin.shared.ui.dd.HorizontalDropLocation;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ValueMap;
+import com.vaadin.terminal.gwt.client.ui.customcomponent.VCustomComponent;
+import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
+import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VHtml5DragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VHtml5File;
+import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
+
+/**
+ *
+ * Must have features pending:
+ *
+ * drop details: locations + sizes in document hierarchy up to wrapper
+ *
+ */
+public class VDragAndDropWrapper extends VCustomComponent implements
+ VHasDropHandler {
+ public static final String DRAG_START_MODE = "dragStartMode";
+ public static final String HTML5_DATA_FLAVORS = "html5-data-flavors";
+
+ private static final String CLASSNAME = "v-ddwrapper";
+ protected static final String DRAGGABLE = "draggable";
+
+ boolean hasTooltip = false;
+
+ public VDragAndDropWrapper() {
+ super();
+
+ hookHtml5Events(getElement());
+ setStyleName(CLASSNAME);
+ addDomHandler(new MouseDownHandler() {
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ if (startDrag(event.getNativeEvent())) {
+ event.preventDefault(); // prevent text selection
+ }
+ }
+ }, MouseDownEvent.getType());
+
+ addDomHandler(new TouchStartHandler() {
+
+ @Override
+ public void onTouchStart(TouchStartEvent event) {
+ if (startDrag(event.getNativeEvent())) {
+ /*
+ * Dont let eg. panel start scrolling.
+ */
+ event.stopPropagation();
+ }
+ }
+ }, TouchStartEvent.getType());
+
+ sinkEvents(Event.TOUCHEVENTS);
+ }
+
+ /**
+ * Starts a drag and drop operation from mousedown or touchstart event if
+ * required conditions are met.
+ *
+ * @param event
+ * @return true if the event was handled as a drag start event
+ */
+ private boolean startDrag(NativeEvent event) {
+ if (dragStartMode == WRAPPER || dragStartMode == COMPONENT) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client).getConnector(
+ VDragAndDropWrapper.this));
+
+ ComponentConnector paintable = Util.findPaintable(client,
+ (Element) event.getEventTarget().cast());
+ Widget widget = paintable.getWidget();
+ transferable.setData("component", paintable);
+ VDragEvent dragEvent = VDragAndDropManager.get().startDrag(
+ transferable, event, true);
+
+ transferable.setData("mouseDown", MouseEventDetailsBuilder
+ .buildMouseEventDetails(event).serialize());
+
+ if (dragStartMode == WRAPPER) {
+ dragEvent.createDragImage(getElement(), true);
+ } else {
+ dragEvent.createDragImage(widget.getElement(), true);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ protected final static int NONE = 0;
+ protected final static int COMPONENT = 1;
+ protected final static int WRAPPER = 2;
+ protected final static int HTML5 = 3;
+
+ protected int dragStartMode;
+
+ ApplicationConnection client;
+ VAbstractDropHandler dropHandler;
+ private VDragEvent vaadinDragEvent;
+
+ int filecounter = 0;
+ Map<String, String> fileIdToReceiver;
+ ValueMap html5DataFlavors;
+ private Element dragStartElement;
+
+ protected void initDragStartMode() {
+ Element div = getElement();
+ if (dragStartMode == HTML5) {
+ if (dragStartElement == null) {
+ dragStartElement = getDragStartElement();
+ dragStartElement.setPropertyBoolean(DRAGGABLE, true);
+ VConsole.log("draggable = "
+ + dragStartElement.getPropertyBoolean(DRAGGABLE));
+ hookHtml5DragStart(dragStartElement);
+ VConsole.log("drag start listeners hooked.");
+ }
+ } else {
+ dragStartElement = null;
+ if (div.hasAttribute(DRAGGABLE)) {
+ div.removeAttribute(DRAGGABLE);
+ }
+ }
+ }
+
+ protected Element getDragStartElement() {
+ return getElement();
+ }
+
+ private boolean uploading;
+
+ private ReadyStateChangeHandler readyStateChangeHandler = new ReadyStateChangeHandler() {
+
+ @Override
+ public void onReadyStateChange(XMLHttpRequest xhr) {
+ if (xhr.getReadyState() == XMLHttpRequest.DONE) {
+ // visit server for possible
+ // variable changes
+ client.sendPendingVariableChanges();
+ uploading = false;
+ startNextUpload();
+ xhr.clearOnReadyStateChange();
+ }
+ }
+ };
+ private Timer dragleavetimer;
+
+ void startNextUpload() {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ if (!uploading) {
+ if (fileIds.size() > 0) {
+
+ uploading = true;
+ final Integer fileId = fileIds.remove(0);
+ VHtml5File file = files.remove(0);
+ final String receiverUrl = client
+ .translateVaadinUri(fileIdToReceiver
+ .remove(fileId.toString()));
+ ExtendedXHR extendedXHR = (ExtendedXHR) ExtendedXHR
+ .create();
+ extendedXHR
+ .setOnReadyStateChange(readyStateChangeHandler);
+ extendedXHR.open("POST", receiverUrl);
+ extendedXHR.postFile(file);
+ }
+ }
+
+ }
+ });
+
+ }
+
+ public boolean html5DragStart(VHtml5DragEvent event) {
+ if (dragStartMode == HTML5) {
+ /*
+ * Populate html5 payload with dataflavors from the serverside
+ */
+ JsArrayString flavors = html5DataFlavors.getKeyArray();
+ for (int i = 0; i < flavors.length(); i++) {
+ String flavor = flavors.get(i);
+ event.setHtml5DataFlavor(flavor,
+ html5DataFlavors.getString(flavor));
+ }
+ event.setEffectAllowed("copy");
+ return true;
+ }
+ return false;
+ }
+
+ public boolean html5DragEnter(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+ try {
+ if (dragleavetimer != null) {
+ // returned quickly back to wrapper
+ dragleavetimer.cancel();
+ dragleavetimer = null;
+ }
+ if (VDragAndDropManager.get().getCurrentDropHandler() != getDropHandler()) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client)
+ .getConnector(this));
+
+ vaadinDragEvent = VDragAndDropManager.get().startDrag(
+ transferable, event, false);
+ VDragAndDropManager.get().setCurrentDropHandler(
+ getDropHandler());
+ }
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+ }
+
+ public boolean html5DragLeave(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+
+ try {
+ dragleavetimer = new Timer() {
+
+ @Override
+ public void run() {
+ // Yes, dragleave happens before drop. Makes no sense to me.
+ // IMO shouldn't fire leave at all if drop happens (I guess
+ // this
+ // is what IE does).
+ // In Vaadin we fire it only if drop did not happen.
+ if (vaadinDragEvent != null
+ && VDragAndDropManager.get()
+ .getCurrentDropHandler() == getDropHandler()) {
+ VDragAndDropManager.get().interruptDrag();
+ }
+ }
+ };
+ dragleavetimer.schedule(350);
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+ }
+
+ public boolean html5DragOver(VHtml5DragEvent event) {
+ if (dropHandler == null) {
+ return true;
+ }
+
+ if (dragleavetimer != null) {
+ // returned quickly back to wrapper
+ dragleavetimer.cancel();
+ dragleavetimer = null;
+ }
+
+ vaadinDragEvent.setCurrentGwtEvent(event);
+ getDropHandler().dragOver(vaadinDragEvent);
+
+ String s = event.getEffectAllowed();
+ if ("all".equals(s) || s.contains("opy")) {
+ event.setDropEffect("copy");
+ } else {
+ event.setDropEffect(s);
+ }
+
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ }
+
+ public boolean html5DragDrop(VHtml5DragEvent event) {
+ if (dropHandler == null || !currentlyValid) {
+ return true;
+ }
+ try {
+
+ VTransferable transferable = vaadinDragEvent.getTransferable();
+
+ JsArrayString types = event.getTypes();
+ for (int i = 0; i < types.length(); i++) {
+ String type = types.get(i);
+ if (isAcceptedType(type)) {
+ String data = event.getDataAsText(type);
+ if (data != null) {
+ transferable.setData(type, data);
+ }
+ }
+ }
+
+ int fileCount = event.getFileCount();
+ if (fileCount > 0) {
+ transferable.setData("filecount", fileCount);
+ for (int i = 0; i < fileCount; i++) {
+ final int fileId = filecounter++;
+ final VHtml5File file = event.getFile(i);
+ transferable.setData("fi" + i, "" + fileId);
+ transferable.setData("fn" + i, file.getName());
+ transferable.setData("ft" + i, file.getType());
+ transferable.setData("fs" + i, file.getSize());
+ queueFilePost(fileId, file);
+ }
+
+ }
+
+ VDragAndDropManager.get().endDrag();
+ vaadinDragEvent = null;
+ try {
+ event.preventDefault();
+ event.stopPropagation();
+ } catch (Exception e) {
+ // VConsole.log("IE9 fails");
+ }
+ return false;
+ } catch (Exception e) {
+ GWT.getUncaughtExceptionHandler().onUncaughtException(e);
+ return true;
+ }
+
+ }
+
+ protected String[] acceptedTypes = new String[] { "Text", "Url",
+ "text/html", "text/plain", "text/rtf" };
+
+ private boolean isAcceptedType(String type) {
+ for (String t : acceptedTypes) {
+ if (t.equals(type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static class ExtendedXHR extends XMLHttpRequest {
+
+ protected ExtendedXHR() {
+ }
+
+ public final native void postFile(VHtml5File file)
+ /*-{
+
+ this.setRequestHeader('Content-Type', 'multipart/form-data');
+ this.send(file);
+ }-*/;
+
+ }
+
+ /**
+ * Currently supports only FF36 as no other browser supports natively File
+ * api.
+ *
+ * @param fileId
+ * @param data
+ */
+ List<Integer> fileIds = new ArrayList<Integer>();
+ List<VHtml5File> files = new ArrayList<VHtml5File>();
+
+ private void queueFilePost(final int fileId, final VHtml5File file) {
+ fileIds.add(fileId);
+ files.add(file);
+ }
+
+ @Override
+ public VDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ protected VerticalDropLocation verticalDropLocation;
+ protected HorizontalDropLocation horizontalDropLocation;
+ private VerticalDropLocation emphasizedVDrop;
+ private HorizontalDropLocation emphasizedHDrop;
+
+ /**
+ * Flag used by html5 dd
+ */
+ private boolean currentlyValid;
+
+ private static final String OVER_STYLE = "v-ddwrapper-over";
+
+ public class CustomDropHandler extends VAbstractDropHandler {
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ updateDropDetails(drag);
+ currentlyValid = false;
+ super.dragEnter(drag);
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis(true);
+ dragleavetimer = null;
+ }
+
+ @Override
+ public void dragOver(final VDragEvent drag) {
+ boolean detailsChanged = updateDropDetails(drag);
+ if (detailsChanged) {
+ currentlyValid = false;
+ validate(new VAcceptCallback() {
+
+ @Override
+ public void accepted(VDragEvent event) {
+ dragAccepted(drag);
+ }
+ }, drag);
+ }
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ deEmphasis(true);
+
+ Map<String, Object> dd = drag.getDropDetails();
+
+ // this is absolute layout based, and we may want to set
+ // component
+ // relatively to where the drag ended.
+ // need to add current location of the drop area
+
+ int absoluteLeft = getAbsoluteLeft();
+ int absoluteTop = getAbsoluteTop();
+
+ dd.put("absoluteLeft", absoluteLeft);
+ dd.put("absoluteTop", absoluteTop);
+
+ if (verticalDropLocation != null) {
+ dd.put("verticalLocation", verticalDropLocation.toString());
+ dd.put("horizontalLocation", horizontalDropLocation.toString());
+ }
+
+ return super.drop(drag);
+ }
+
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ currentlyValid = true;
+ emphasis(drag);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(
+ VDragAndDropWrapper.this);
+ }
+
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ }
+
+ protected native void hookHtml5DragStart(Element el)
+ /*-{
+ var me = this;
+ el.addEventListener("dragstart", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+ }-*/;
+
+ /**
+ * Prototype code, memory leak risk.
+ *
+ * @param el
+ */
+ protected native void hookHtml5Events(Element el)
+ /*-{
+ var me = this;
+
+ el.addEventListener("dragenter", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+
+ el.addEventListener("dragleave", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+
+ el.addEventListener("dragover", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+
+ el.addEventListener("drop", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }), false);
+ }-*/;
+
+ public boolean updateDropDetails(VDragEvent drag) {
+ VerticalDropLocation oldVL = verticalDropLocation;
+ verticalDropLocation = DDUtil.getVerticalDropLocation(getElement(),
+ drag.getCurrentGwtEvent(), 0.2);
+ drag.getDropDetails().put("verticalLocation",
+ verticalDropLocation.toString());
+ HorizontalDropLocation oldHL = horizontalDropLocation;
+ horizontalDropLocation = DDUtil.getHorizontalDropLocation(getElement(),
+ drag.getCurrentGwtEvent(), 0.2);
+ drag.getDropDetails().put("horizontalLocation",
+ horizontalDropLocation.toString());
+ if (oldHL != horizontalDropLocation || oldVL != verticalDropLocation) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected void deEmphasis(boolean doLayout) {
+ if (emphasizedVDrop != null) {
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + emphasizedVDrop.toString().toLowerCase(), false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + emphasizedHDrop.toString().toLowerCase(), false);
+ }
+ if (doLayout) {
+ notifySizePotentiallyChanged();
+ }
+ }
+
+ private void notifySizePotentiallyChanged() {
+ LayoutManager.get(client).setNeedsMeasure(
+ ConnectorMap.get(client).getConnector(getElement()));
+ }
+
+ protected void emphasis(VDragEvent drag) {
+ deEmphasis(false);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE, true);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + verticalDropLocation.toString().toLowerCase(), true);
+ VDragAndDropWrapper.setStyleName(getElement(), OVER_STYLE + "-"
+ + horizontalDropLocation.toString().toLowerCase(), true);
+ emphasizedVDrop = verticalDropLocation;
+ emphasizedHDrop = horizontalDropLocation;
+
+ // TODO build (to be an example) an emphasis mode where drag image
+ // is fitted before or after the content
+ notifySizePotentiallyChanged();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java
new file mode 100644
index 0000000000..bb511524e5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/draganddropwrapper/VDragAndDropWrapperIE.java
@@ -0,0 +1,69 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.draganddropwrapper;
+
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.Element;
+import com.vaadin.terminal.gwt.client.VConsole;
+
+public class VDragAndDropWrapperIE extends VDragAndDropWrapper {
+ private AnchorElement anchor = null;
+
+ @Override
+ protected Element getDragStartElement() {
+ VConsole.log("IE get drag start element...");
+ Element div = getElement();
+ if (dragStartMode == HTML5) {
+ if (anchor == null) {
+ anchor = Document.get().createAnchorElement();
+ anchor.setHref("#");
+ anchor.setClassName("drag-start");
+ div.appendChild(anchor);
+ }
+ VConsole.log("IE get drag start element...");
+ return (Element) anchor.cast();
+ } else {
+ if (anchor != null) {
+ div.removeChild(anchor);
+ anchor = null;
+ }
+ return div;
+ }
+ }
+
+ @Override
+ protected native void hookHtml5DragStart(Element el)
+ /*-{
+ var me = this;
+
+ el.attachEvent("ondragstart", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragStart(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }));
+ }-*/;
+
+ @Override
+ protected native void hookHtml5Events(Element el)
+ /*-{
+ var me = this;
+
+ el.attachEvent("ondragenter", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragEnter(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }));
+
+ el.attachEvent("ondragleave", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragLeave(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }));
+
+ el.attachEvent("ondragover", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragOver(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }));
+
+ el.attachEvent("ondrop", $entry(function(ev) {
+ return me.@com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper::html5DragDrop(Lcom/vaadin/terminal/gwt/client/ui/dd/VHtml5DragEvent;)(ev);
+ }));
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java
new file mode 100644
index 0000000000..a1851d9c84
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/EmbeddedConnector.java
@@ -0,0 +1,222 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.embedded;
+
+import java.util.Map;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.NativeEvent;
+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.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.embedded.EmbeddedServerRpc;
+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.VConsole;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.ui.Embedded;
+
+@Connect(Embedded.class)
+public class EmbeddedConnector extends AbstractComponentConnector implements
+ Paintable {
+
+ public static final String ALTERNATE_TEXT = "alt";
+
+ EmbeddedServerRpc rpc;
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(EmbeddedServerRpc.class, this);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // Save details
+ getWidget().client = client;
+
+ boolean clearBrowserElement = true;
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ if (uidl.hasAttribute("type")) {
+ // remove old style name related to type
+ if (getWidget().type != null) {
+ getWidget().removeStyleName(
+ VEmbedded.CLASSNAME + "-" + getWidget().type);
+ }
+ // remove old style name related to mime type
+ if (getWidget().mimetype != null) {
+ getWidget().removeStyleName(
+ VEmbedded.CLASSNAME + "-" + getWidget().mimetype);
+ }
+ getWidget().type = uidl.getStringAttribute("type");
+ if (getWidget().type.equals("image")) {
+ getWidget().addStyleName(VEmbedded.CLASSNAME + "-image");
+ Element el = null;
+ boolean created = false;
+ NodeList<Node> nodes = getWidget().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) {
+ getWidget().setHTML("");
+ el = DOM.createImg();
+ created = true;
+ DOM.sinkEvents(el, Event.ONLOAD);
+ }
+
+ // Set attributes
+ Style style = el.getStyle();
+ style.setProperty("width", getState().getWidth());
+ style.setProperty("height", getState().getHeight());
+
+ DOM.setElementProperty(el, "src",
+ getWidget().getSrc(uidl, client));
+
+ if (uidl.hasAttribute(ALTERNATE_TEXT)) {
+ el.setPropertyString(ALTERNATE_TEXT,
+ uidl.getStringAttribute(ALTERNATE_TEXT));
+ }
+
+ if (created) {
+ // insert in dom late
+ getWidget().getElement().appendChild(el);
+ }
+
+ /*
+ * Sink tooltip events so tooltip is displayed when hovering the
+ * image.
+ */
+ getWidget().sinkEvents(VTooltip.TOOLTIP_EVENTS);
+
+ } else if (getWidget().type.equals("browser")) {
+ getWidget().addStyleName(VEmbedded.CLASSNAME + "-browser");
+ if (getWidget().browserElement == null) {
+ getWidget().setHTML(
+ "<iframe width=\"100%\" height=\"100%\" frameborder=\"0\""
+ + " allowTransparency=\"true\" src=\"\""
+ + " name=\"" + uidl.getId()
+ + "\"></iframe>");
+ getWidget().browserElement = DOM.getFirstChild(getWidget()
+ .getElement());
+ }
+ DOM.setElementAttribute(getWidget().browserElement, "src",
+ getWidget().getSrc(uidl, client));
+ clearBrowserElement = false;
+ } else {
+ VConsole.log("Unknown Embedded type '" + getWidget().type + "'");
+ }
+ } else if (uidl.hasAttribute("mimetype")) {
+ // remove old style name related to type
+ if (getWidget().type != null) {
+ getWidget().removeStyleName(
+ VEmbedded.CLASSNAME + "-" + getWidget().type);
+ }
+ // remove old style name related to mime type
+ if (getWidget().mimetype != null) {
+ getWidget().removeStyleName(
+ VEmbedded.CLASSNAME + "-" + getWidget().mimetype);
+ }
+ final String mime = uidl.getStringAttribute("mimetype");
+ if (mime.equals("application/x-shockwave-flash")) {
+ getWidget().mimetype = "flash";
+ // Handle embedding of Flash
+ getWidget().addStyleName(VEmbedded.CLASSNAME + "-flash");
+ getWidget().setHTML(getWidget().createFlashEmbed(uidl));
+
+ } else if (mime.equals("image/svg+xml")) {
+ getWidget().mimetype = "svg";
+ getWidget().addStyleName(VEmbedded.CLASSNAME + "-svg");
+ String data;
+ Map<String, String> parameters = VEmbedded.getParameters(uidl);
+ if (parameters.get("data") == null) {
+ data = getWidget().getSrc(uidl, client);
+ } else {
+ data = "data:image/svg+xml," + parameters.get("data");
+ }
+ getWidget().setHTML("");
+ ObjectElement obj = Document.get().createObjectElement();
+ obj.setType(mime);
+ obj.setData(data);
+ if (!isUndefinedWidth()) {
+ obj.getStyle().setProperty("width", "100%");
+ }
+ if (!isUndefinedHeight()) {
+ obj.getStyle().setProperty("height", "100%");
+ }
+ if (uidl.hasAttribute("classid")) {
+ obj.setAttribute("classid",
+ uidl.getStringAttribute("classid"));
+ }
+ if (uidl.hasAttribute("codebase")) {
+ obj.setAttribute("codebase",
+ uidl.getStringAttribute("codebase"));
+ }
+ if (uidl.hasAttribute("codetype")) {
+ obj.setAttribute("codetype",
+ uidl.getStringAttribute("codetype"));
+ }
+ if (uidl.hasAttribute("archive")) {
+ obj.setAttribute("archive",
+ uidl.getStringAttribute("archive"));
+ }
+ if (uidl.hasAttribute("standby")) {
+ obj.setAttribute("standby",
+ uidl.getStringAttribute("standby"));
+ }
+ getWidget().getElement().appendChild(obj);
+ if (uidl.hasAttribute(ALTERNATE_TEXT)) {
+ obj.setInnerText(uidl.getStringAttribute(ALTERNATE_TEXT));
+ }
+ } else {
+ VConsole.log("Unknown Embedded mimetype '" + mime + "'");
+ }
+ } else {
+ VConsole.log("Unknown Embedded; no type or mimetype attribute");
+ }
+
+ if (clearBrowserElement) {
+ getWidget().browserElement = null;
+ }
+ }
+
+ @Override
+ public VEmbedded getWidget() {
+ return (VEmbedded) super.getWidget();
+ }
+
+ protected final ClickEventHandler clickEventHandler = new ClickEventHandler(
+ this) {
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+
+ };
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java
new file mode 100644
index 0000000000..1d2a5a156a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/embedded/VEmbedded.java
@@ -0,0 +1,238 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.embedded;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+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.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+
+public class VEmbedded extends HTML {
+ public static String CLASSNAME = "v-embedded";
+
+ protected Element browserElement;
+
+ protected String type;
+ protected String mimetype;
+
+ protected ApplicationConnection client;
+
+ public VEmbedded() {
+ setStyleName(CLASSNAME);
+ }
+
+ /**
+ * Creates the Object and Embed tags for the Flash plugin so it works
+ * cross-browser
+ *
+ * @param uidl
+ * The UIDL
+ * @return Tags concatenated into a string
+ */
+ protected String createFlashEmbed(UIDL uidl) {
+ /*
+ * To ensure cross-browser compatibility we are using the twice-cooked
+ * method to embed flash i.e. we add a OBJECT tag for IE ActiveX and
+ * inside it a EMBED for all other browsers.
+ */
+
+ StringBuilder html = new StringBuilder();
+
+ // Start the object tag
+ html.append("<object ");
+
+ /*
+ * Add classid required for ActiveX to recognize the flash. This is a
+ * predefined value which ActiveX recognizes and must be the given
+ * value. More info can be found on
+ * http://kb2.adobe.com/cps/415/tn_4150.html. Allow user to override
+ * this by setting his own classid.
+ */
+ if (uidl.hasAttribute("classid")) {
+ html.append("classid=\""
+ + Util.escapeAttribute(uidl.getStringAttribute("classid"))
+ + "\" ");
+ } else {
+ html.append("classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" ");
+ }
+
+ /*
+ * Add codebase required for ActiveX and must be exactly this according
+ * to http://kb2.adobe.com/cps/415/tn_4150.html to work with the above
+ * given classid. Again, see more info on
+ * http://kb2.adobe.com/cps/415/tn_4150.html. Limiting Flash version to
+ * 6.0.0.0 and above. Allow user to override this by setting his own
+ * codebase
+ */
+ if (uidl.hasAttribute("codebase")) {
+ html.append("codebase=\""
+ + Util.escapeAttribute(uidl.getStringAttribute("codebase"))
+ + "\" ");
+ } else {
+ html.append("codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0\" ");
+ }
+
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ String height = paintable.getState().getHeight();
+ String width = paintable.getState().getWidth();
+
+ // Add width and height
+ html.append("width=\"" + Util.escapeAttribute(width) + "\" ");
+ html.append("height=\"" + Util.escapeAttribute(height) + "\" ");
+ html.append("type=\"application/x-shockwave-flash\" ");
+
+ // Codetype
+ if (uidl.hasAttribute("codetype")) {
+ html.append("codetype=\""
+ + Util.escapeAttribute(uidl.getStringAttribute("codetype"))
+ + "\" ");
+ }
+
+ // Standby
+ if (uidl.hasAttribute("standby")) {
+ html.append("standby=\""
+ + Util.escapeAttribute(uidl.getStringAttribute("standby"))
+ + "\" ");
+ }
+
+ // Archive
+ if (uidl.hasAttribute("archive")) {
+ html.append("archive=\""
+ + Util.escapeAttribute(uidl.getStringAttribute("archive"))
+ + "\" ");
+ }
+
+ // End object tag
+ html.append(">");
+
+ // Ensure we have an movie parameter
+ Map<String, String> parameters = getParameters(uidl);
+ if (parameters.get("movie") == null) {
+ parameters.put("movie", getSrc(uidl, client));
+ }
+
+ // Add parameters to OBJECT
+ for (String name : parameters.keySet()) {
+ html.append("<param ");
+ html.append("name=\"" + Util.escapeAttribute(name) + "\" ");
+ html.append("value=\"" + Util.escapeAttribute(parameters.get(name))
+ + "\" ");
+ html.append("/>");
+ }
+
+ // Build inner EMBED tag
+ html.append("<embed ");
+ html.append("src=\"" + Util.escapeAttribute(getSrc(uidl, client))
+ + "\" ");
+ html.append("width=\"" + Util.escapeAttribute(width) + "\" ");
+ html.append("height=\"" + Util.escapeAttribute(height) + "\" ");
+ html.append("type=\"application/x-shockwave-flash\" ");
+
+ // Add the parameters to the Embed
+ for (String name : parameters.keySet()) {
+ html.append(Util.escapeAttribute(name));
+ html.append("=");
+ html.append("\"" + Util.escapeAttribute(parameters.get(name))
+ + "\"");
+ }
+
+ // End embed tag
+ html.append("></embed>");
+
+ if (uidl.hasAttribute(EmbeddedConnector.ALTERNATE_TEXT)) {
+ html.append(uidl
+ .getStringAttribute(EmbeddedConnector.ALTERNATE_TEXT));
+ }
+
+ // End object tag
+ html.append("</object>");
+
+ return html.toString();
+ }
+
+ /**
+ * Returns a map (name -> value) of all parameters in the UIDL.
+ *
+ * @param uidl
+ * @return
+ */
+ protected static Map<String, String> getParameters(UIDL uidl) {
+ Map<String, String> parameters = new HashMap<String, String>();
+
+ Iterator<Object> childIterator = uidl.getChildIterator();
+ while (childIterator.hasNext()) {
+
+ Object child = childIterator.next();
+ if (child instanceof UIDL) {
+
+ UIDL childUIDL = (UIDL) child;
+ if (childUIDL.getTag().equals("embeddedparam")) {
+ String name = childUIDL.getStringAttribute("name");
+ String value = childUIDL.getStringAttribute("value");
+ parameters.put(name, value);
+ }
+ }
+
+ }
+
+ return parameters;
+ }
+
+ /**
+ * Helper to return translated src-attribute from embedded's UIDL
+ *
+ * @param uidl
+ * @param client
+ * @return
+ */
+ protected String getSrc(UIDL uidl, ApplicationConnection client) {
+ String url = client.translateVaadinUri(uidl.getStringAttribute("src"));
+ if (url == null) {
+ return "";
+ }
+ return url;
+ }
+
+ @Override
+ protected void onDetach() {
+ if (BrowserInfo.get().isIE()) {
+ // Force browser to fire unload event when component is detached
+ // from the view (IE doesn't do this automatically)
+ if (browserElement != null) {
+ /*
+ * src was previously set to javascript:false, but this was not
+ * enough to overcome a bug when detaching an iframe with a pdf
+ * loaded in IE9. about:blank seems to cause the adobe reader
+ * plugin to unload properly before the iframe is removed. See
+ * #7855
+ */
+ DOM.setElementAttribute(browserElement, "src", "about:blank");
+ }
+ }
+ super.onDetach();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (DOM.eventGetType(event) == Event.ONLOAD) {
+ VConsole.log("Embeddable onload");
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java
new file mode 100644
index 0000000000..e31de1f85d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/form/FormConnector.java
@@ -0,0 +1,207 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.form;
+
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.form.FormState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeEvent;
+import com.vaadin.terminal.gwt.client.ui.layout.ElementResizeListener;
+import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.ui.Form;
+
+@Connect(Form.class)
+public class FormConnector extends AbstractComponentContainerConnector
+ implements Paintable, MayScrollChildren {
+
+ private final ElementResizeListener footerResizeListener = new ElementResizeListener() {
+ @Override
+ public void onElementResize(ElementResizeEvent e) {
+ VForm form = getWidget();
+
+ int footerHeight;
+ if (form.footer != null) {
+ LayoutManager lm = getLayoutManager();
+ footerHeight = lm.getOuterHeight(form.footer.getElement());
+ } else {
+ footerHeight = 0;
+ }
+
+ form.fieldContainer.getStyle().setPaddingBottom(footerHeight,
+ Unit.PX);
+ form.footerContainer.getStyle()
+ .setMarginTop(-footerHeight, Unit.PX);
+ }
+ };
+
+ @Override
+ public void onUnregister() {
+ VForm form = getWidget();
+ if (form.footer != null) {
+ getLayoutManager().removeElementResizeListener(
+ form.footer.getElement(), footerResizeListener);
+ }
+ }
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().client = client;
+ getWidget().id = uidl.getId();
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ boolean legendEmpty = true;
+ if (getState().getCaption() != null) {
+ getWidget().caption.setInnerText(getState().getCaption());
+ legendEmpty = false;
+ } else {
+ getWidget().caption.setInnerText("");
+ }
+ if (getState().getIcon() != null) {
+ if (getWidget().icon == null) {
+ getWidget().icon = new Icon(client);
+ getWidget().legend.insertFirst(getWidget().icon.getElement());
+ }
+ getWidget().icon.setUri(getState().getIcon().getURL());
+ legendEmpty = false;
+ } else {
+ if (getWidget().icon != null) {
+ getWidget().legend.removeChild(getWidget().icon.getElement());
+ }
+ }
+ if (legendEmpty) {
+ getWidget().addStyleDependentName("nocaption");
+ } else {
+ getWidget().removeStyleDependentName("nocaption");
+ }
+
+ if (null != getState().getErrorMessage()) {
+ getWidget().errorMessage
+ .updateMessage(getState().getErrorMessage());
+ getWidget().errorMessage.setVisible(true);
+ } else {
+ getWidget().errorMessage.setVisible(false);
+ }
+
+ if (getState().hasDescription()) {
+ getWidget().desc.setInnerHTML(getState().getDescription());
+ if (getWidget().desc.getParentElement() == null) {
+ getWidget().fieldSet.insertAfter(getWidget().desc,
+ getWidget().legend);
+ }
+ } else {
+ getWidget().desc.setInnerHTML("");
+ if (getWidget().desc.getParentElement() != null) {
+ getWidget().fieldSet.removeChild(getWidget().desc);
+ }
+ }
+
+ // first render footer so it will be easier to handle relative height of
+ // main layout
+ if (getState().getFooter() != null) {
+ // render footer
+ ComponentConnector newFooter = (ComponentConnector) getState()
+ .getFooter();
+ Widget newFooterWidget = newFooter.getWidget();
+ if (getWidget().footer == null) {
+ getLayoutManager().addElementResizeListener(
+ newFooterWidget.getElement(), footerResizeListener);
+ getWidget().add(newFooter.getWidget(),
+ getWidget().footerContainer);
+ getWidget().footer = newFooterWidget;
+ } else if (newFooter != getWidget().footer) {
+ getLayoutManager().removeElementResizeListener(
+ getWidget().footer.getElement(), footerResizeListener);
+ getLayoutManager().addElementResizeListener(
+ newFooterWidget.getElement(), footerResizeListener);
+ getWidget().remove(getWidget().footer);
+ getWidget().add(newFooter.getWidget(),
+ getWidget().footerContainer);
+ }
+ getWidget().footer = newFooterWidget;
+ } else {
+ if (getWidget().footer != null) {
+ getLayoutManager().removeElementResizeListener(
+ getWidget().footer.getElement(), footerResizeListener);
+ getWidget().remove(getWidget().footer);
+ getWidget().footer = null;
+ }
+ }
+
+ ComponentConnector newLayout = (ComponentConnector) getState()
+ .getLayout();
+ Widget newLayoutWidget = newLayout.getWidget();
+ if (getWidget().lo == null) {
+ // Layout not rendered before
+ getWidget().lo = newLayoutWidget;
+ getWidget().add(newLayoutWidget, getWidget().fieldContainer);
+ } else if (getWidget().lo != newLayoutWidget) {
+ // Layout has changed
+ getWidget().remove(getWidget().lo);
+ getWidget().lo = newLayoutWidget;
+ getWidget().add(newLayoutWidget, getWidget().fieldContainer);
+ }
+
+ // also recalculates size of the footer if undefined size form - see
+ // #3710
+ client.runDescendentsLayout(getWidget());
+
+ // We may have actions attached
+ if (uidl.getChildCount() >= 1) {
+ UIDL childUidl = uidl.getChildByTagName("actions");
+ if (childUidl != null) {
+ if (getWidget().shortcutHandler == null) {
+ getWidget().shortcutHandler = new ShortcutActionHandler(
+ getConnectorId(), client);
+ getWidget().keyDownRegistration = getWidget()
+ .addDomHandler(getWidget(), KeyDownEvent.getType());
+ }
+ getWidget().shortcutHandler.updateActionMap(childUidl);
+ }
+ } else if (getWidget().shortcutHandler != null) {
+ getWidget().keyDownRegistration.removeHandler();
+ getWidget().shortcutHandler = null;
+ getWidget().keyDownRegistration = null;
+ }
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP form don't render caption for neither field layout nor footer
+ // layout
+ }
+
+ @Override
+ public VForm getWidget() {
+ return (VForm) super.getWidget();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().isPropertyReadOnly();
+ }
+
+ @Override
+ public FormState getState() {
+ return (FormState) super.getState();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java b/client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java
new file mode 100644
index 0000000000..823b0e83ae
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/form/VForm.java
@@ -0,0 +1,76 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.form;
+
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.VErrorMessage;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+
+public class VForm extends ComplexPanel implements KeyDownHandler {
+
+ protected String id;
+
+ public static final String CLASSNAME = "v-form";
+
+ Widget lo;
+ Element legend = DOM.createLegend();
+ Element caption = DOM.createSpan();
+ Element desc = DOM.createDiv();
+ Icon icon;
+ VErrorMessage errorMessage = new VErrorMessage();
+
+ Element fieldContainer = DOM.createDiv();
+
+ Element footerContainer = DOM.createDiv();
+
+ Element fieldSet = DOM.createFieldSet();
+
+ Widget footer;
+
+ ApplicationConnection client;
+
+ ShortcutActionHandler shortcutHandler;
+
+ HandlerRegistration keyDownRegistration;
+
+ public VForm() {
+ setElement(DOM.createDiv());
+ getElement().appendChild(fieldSet);
+ setStyleName(CLASSNAME);
+ fieldSet.appendChild(legend);
+ legend.appendChild(caption);
+ desc.setClassName("v-form-description");
+ fieldSet.appendChild(desc); // Adding description for initial padding
+ // measurements, removed later if no
+ // description is set
+ fieldContainer.setClassName(CLASSNAME + "-content");
+ fieldSet.appendChild(fieldContainer);
+ errorMessage.setVisible(false);
+ errorMessage.setStyleName(CLASSNAME + "-errormessage");
+ fieldSet.appendChild(errorMessage.getElement());
+ fieldSet.appendChild(footerContainer);
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ shortcutHandler.handleKeyboardEvent(Event.as(event.getNativeEvent()));
+ }
+
+ @Override
+ protected void add(Widget child, Element container) {
+ // Overridden to allow VFormPaintable to call this. Should be removed
+ // once functionality from VFormPaintable is moved to VForm.
+ super.add(child, container);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java
new file mode 100644
index 0000000000..567513d7fe
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/FormLayoutConnector.java
@@ -0,0 +1,135 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.formlayout;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector;
+import com.vaadin.terminal.gwt.client.ui.formlayout.VFormLayout.Caption;
+import com.vaadin.terminal.gwt.client.ui.formlayout.VFormLayout.ErrorFlag;
+import com.vaadin.terminal.gwt.client.ui.formlayout.VFormLayout.VFormLayoutTable;
+import com.vaadin.ui.FormLayout;
+
+@Connect(FormLayout.class)
+public class FormLayoutConnector extends AbstractLayoutConnector {
+
+ @Override
+ public AbstractOrderedLayoutState getState() {
+ return (AbstractOrderedLayoutState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ VFormLayoutTable formLayoutTable = getWidget().table;
+
+ formLayoutTable.setMargins(new VMarginInfo(getState()
+ .getMarginsBitmask()));
+ formLayoutTable.setSpacing(getState().isSpacing());
+
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ VFormLayout formLayout = getWidget();
+ VFormLayoutTable formLayoutTable = getWidget().table;
+
+ int childId = 0;
+
+ formLayoutTable.setRowCount(getChildComponents().size());
+
+ for (ComponentConnector child : getChildComponents()) {
+ Widget childWidget = child.getWidget();
+
+ Caption caption = formLayoutTable.getCaption(childWidget);
+ if (caption == null) {
+ caption = formLayout.new Caption(child);
+ caption.addClickHandler(formLayoutTable);
+ }
+
+ ErrorFlag error = formLayoutTable.getError(childWidget);
+ if (error == null) {
+ error = formLayout.new ErrorFlag(child);
+ }
+
+ formLayoutTable.setChild(childId, childWidget, caption, error);
+ childId++;
+ }
+
+ for (ComponentConnector oldChild : event.getOldChildren()) {
+ if (oldChild.getParent() == this) {
+ continue;
+ }
+
+ formLayoutTable.cleanReferences(oldChild.getWidget());
+ }
+
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ getWidget().table.updateCaption(component.getWidget(),
+ component.getState(), component.isEnabled());
+ boolean hideErrors = false;
+
+ // FIXME This incorrectly depends on AbstractFieldConnector
+ if (component instanceof AbstractFieldConnector) {
+ hideErrors = ((AbstractFieldConnector) component).getState()
+ .isHideErrors();
+ }
+
+ getWidget().table.updateError(component.getWidget(), component
+ .getState().getErrorMessage(), hideErrors);
+ }
+
+ @Override
+ public VFormLayout getWidget() {
+ return (VFormLayout) super.getWidget();
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+ TooltipInfo info = null;
+
+ if (element != getWidget().getElement()) {
+ Object node = Util.findWidget(
+ (com.google.gwt.user.client.Element) element,
+ VFormLayout.Caption.class);
+
+ if (node != null) {
+ VFormLayout.Caption caption = (VFormLayout.Caption) node;
+ info = caption.getOwner().getTooltipInfo(element);
+ } else {
+
+ node = Util.findWidget(
+ (com.google.gwt.user.client.Element) element,
+ VFormLayout.ErrorFlag.class);
+
+ if (node != null) {
+ VFormLayout.ErrorFlag flag = (VFormLayout.ErrorFlag) node;
+ info = flag.getOwner().getTooltipInfo(element);
+ }
+ }
+ }
+
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java
new file mode 100644
index 0000000000..9ecab6352c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/formlayout/VFormLayout.java
@@ -0,0 +1,366 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.formlayout;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+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.shared.ComponentState;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.StyleConstants;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+
+/**
+ * Two col Layout that places caption on left col and field on right col
+ */
+public class VFormLayout extends SimplePanel {
+
+ private final static String CLASSNAME = "v-formlayout";
+
+ VFormLayoutTable table;
+
+ public VFormLayout() {
+ super();
+ setStyleName(CLASSNAME);
+ table = new VFormLayoutTable();
+ setWidget(table);
+ }
+
+ /**
+ * Parses the stylenames from shared state
+ *
+ * @param state
+ * shared state of the component
+ * @param enabled
+ * @return An array of stylenames
+ */
+ private String[] getStylesFromState(ComponentState state, boolean enabled) {
+ List<String> styles = new ArrayList<String>();
+ if (state.hasStyles()) {
+ for (String name : state.getStyles()) {
+ styles.add(name);
+ }
+ }
+
+ if (!enabled) {
+ styles.add(ApplicationConnection.DISABLED_CLASSNAME);
+ }
+
+ return styles.toArray(new String[styles.size()]);
+ }
+
+ public class VFormLayoutTable extends FlexTable implements ClickHandler {
+
+ private static final int COLUMN_CAPTION = 0;
+ private static final int COLUMN_ERRORFLAG = 1;
+ private static final int COLUMN_WIDGET = 2;
+
+ private HashMap<Widget, Caption> widgetToCaption = new HashMap<Widget, Caption>();
+ private HashMap<Widget, ErrorFlag> widgetToError = new HashMap<Widget, ErrorFlag>();
+
+ public VFormLayoutTable() {
+ DOM.setElementProperty(getElement(), "cellPadding", "0");
+ DOM.setElementProperty(getElement(), "cellSpacing", "0");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt
+ * .event.dom.client.ClickEvent)
+ */
+ @Override
+ public void onClick(ClickEvent event) {
+ Caption caption = (Caption) event.getSource();
+ if (caption.getOwner() != null) {
+ if (caption.getOwner() instanceof Focusable) {
+ ((Focusable) caption.getOwner()).focus();
+ } else if (caption.getOwner() instanceof com.google.gwt.user.client.ui.Focusable) {
+ ((com.google.gwt.user.client.ui.Focusable) caption
+ .getOwner()).setFocus(true);
+ }
+ }
+ }
+
+ public void setMargins(VMarginInfo 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());
+
+ }
+
+ public void setSpacing(boolean spacing) {
+ setStyleName(getElement(), CLASSNAME + "-" + "spacing", spacing);
+
+ }
+
+ public void setRowCount(int rowNr) {
+ for (int i = 0; i < rowNr; i++) {
+ prepareCell(i, COLUMN_CAPTION);
+ getCellFormatter().setStyleName(i, COLUMN_CAPTION,
+ CLASSNAME + "-captioncell");
+
+ prepareCell(i, 1);
+ getCellFormatter().setStyleName(i, COLUMN_ERRORFLAG,
+ CLASSNAME + "-errorcell");
+
+ prepareCell(i, 2);
+ getCellFormatter().setStyleName(i, COLUMN_WIDGET,
+ CLASSNAME + "-contentcell");
+
+ String rowstyles = CLASSNAME + "-row";
+ if (i == 0) {
+ rowstyles += " " + CLASSNAME + "-firstrow";
+ }
+ if (i == rowNr - 1) {
+ rowstyles += " " + CLASSNAME + "-lastrow";
+ }
+
+ getRowFormatter().setStyleName(i, rowstyles);
+
+ }
+ while (getRowCount() != rowNr) {
+ removeRow(rowNr);
+ }
+ }
+
+ public void setChild(int rowNr, Widget childWidget, Caption caption,
+ ErrorFlag error) {
+ setWidget(rowNr, COLUMN_WIDGET, childWidget);
+ setWidget(rowNr, COLUMN_CAPTION, caption);
+ setWidget(rowNr, COLUMN_ERRORFLAG, error);
+
+ widgetToCaption.put(childWidget, caption);
+ widgetToError.put(childWidget, error);
+
+ }
+
+ public Caption getCaption(Widget childWidget) {
+ return widgetToCaption.get(childWidget);
+ }
+
+ public ErrorFlag getError(Widget childWidget) {
+ return widgetToError.get(childWidget);
+ }
+
+ public void cleanReferences(Widget oldChildWidget) {
+ widgetToError.remove(oldChildWidget);
+ widgetToCaption.remove(oldChildWidget);
+
+ }
+
+ public void updateCaption(Widget widget, ComponentState state,
+ boolean enabled) {
+ final Caption c = widgetToCaption.get(widget);
+ if (c != null) {
+ c.updateCaption(state, enabled);
+ }
+ }
+
+ public void updateError(Widget widget, String errorMessage,
+ boolean hideErrors) {
+ final ErrorFlag e = widgetToError.get(widget);
+ if (e != null) {
+ e.updateError(errorMessage, hideErrors);
+ }
+
+ }
+
+ }
+
+ // TODO why duplicated here?
+ public class Caption extends HTML {
+
+ public static final String CLASSNAME = "v-caption";
+
+ private final ComponentConnector owner;
+
+ private Element requiredFieldIndicator;
+
+ private Icon icon;
+
+ private Element captionText;
+
+ /**
+ *
+ * @param component
+ * optional owner of caption. If not set, getOwner will
+ * return null
+ */
+ public Caption(ComponentConnector component) {
+ super();
+ owner = component;
+ }
+
+ private void setStyles(String[] styles) {
+ String styleName = CLASSNAME;
+
+ if (styles != null) {
+ for (String style : styles) {
+ if (ApplicationConnection.DISABLED_CLASSNAME.equals(style)) {
+ // Add v-disabled also without classname prefix so
+ // generic v-disabled CSS rules work
+ styleName += " " + style;
+ }
+
+ styleName += " " + CLASSNAME + "-" + style;
+ }
+ }
+
+ setStyleName(styleName);
+ }
+
+ public void updateCaption(ComponentState state, boolean enabled) {
+ // Update styles as they might have changed when the caption changed
+ setStyles(getStylesFromState(state, enabled));
+
+ boolean isEmpty = true;
+
+ if (state.getIcon() != null) {
+ if (icon == null) {
+ icon = new Icon(owner.getConnection());
+
+ DOM.insertChild(getElement(), icon.getElement(), 0);
+ }
+ icon.setUri(state.getIcon().getURL());
+ isEmpty = false;
+ } else {
+ if (icon != null) {
+ DOM.removeChild(getElement(), icon.getElement());
+ icon = null;
+ }
+
+ }
+
+ if (state.getCaption() != null) {
+ if (captionText == null) {
+ captionText = DOM.createSpan();
+ DOM.insertChild(getElement(), captionText, icon == null ? 0
+ : 1);
+ }
+ String c = state.getCaption();
+ if (c == null) {
+ c = "";
+ } else {
+ isEmpty = false;
+ }
+ DOM.setInnerText(captionText, c);
+ } else {
+ // TODO should span also be removed
+ }
+
+ if (state.hasDescription() && captionText != null) {
+ addStyleDependentName("hasdescription");
+ } else {
+ removeStyleDependentName("hasdescription");
+ }
+
+ boolean required = owner instanceof AbstractFieldConnector
+ && ((AbstractFieldConnector) owner).isRequired();
+ if (required) {
+ if (requiredFieldIndicator == null) {
+ requiredFieldIndicator = DOM.createSpan();
+ DOM.setInnerText(requiredFieldIndicator, "*");
+ DOM.setElementProperty(requiredFieldIndicator, "className",
+ "v-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...
+ // FIXME: This could be unnecessary for IE8+
+ if (BrowserInfo.get().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 ComponentConnector getOwner() {
+ return owner;
+ }
+ }
+
+ class ErrorFlag extends HTML {
+ private static final String CLASSNAME = VFormLayout.CLASSNAME
+ + "-error-indicator";
+ Element errorIndicatorElement;
+
+ private ComponentConnector owner;
+
+ public ErrorFlag(ComponentConnector owner) {
+ setStyleName(CLASSNAME);
+ sinkEvents(VTooltip.TOOLTIP_EVENTS);
+ this.owner = owner;
+ }
+
+ public ComponentConnector getOwner() {
+ return owner;
+ }
+
+ public void updateError(String errorMessage, boolean hideErrors) {
+ boolean showError = null != errorMessage;
+ if (hideErrors) {
+ showError = false;
+ }
+
+ if (showError) {
+ if (errorIndicatorElement == null) {
+ errorIndicatorElement = DOM.createDiv();
+ DOM.setInnerHTML(errorIndicatorElement, "&nbsp;");
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "v-errorindicator");
+ DOM.appendChild(getElement(), errorIndicatorElement);
+ }
+
+ } else if (errorIndicatorElement != null) {
+ DOM.removeChild(getElement(), errorIndicatorElement);
+ errorIndicatorElement = null;
+ }
+ }
+
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java
new file mode 100644
index 0000000000..07e481d31d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/GridLayoutConnector.java
@@ -0,0 +1,240 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.gridlayout;
+
+import java.util.Iterator;
+
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.AlignmentInfo;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.LayoutClickRpc;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc;
+import com.vaadin.shared.ui.gridlayout.GridLayoutState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.gridlayout.VGridLayout.Cell;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+import com.vaadin.ui.GridLayout;
+
+@Connect(GridLayout.class)
+public class GridLayoutConnector extends AbstractComponentContainerConnector
+ implements Paintable, DirectionalManagedLayout {
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return getWidget().getComponent(element);
+ }
+
+ @Override
+ protected LayoutClickRpc getLayoutClickRPC() {
+ return rpc;
+ };
+
+ };
+
+ private GridLayoutServerRpc rpc;
+ private boolean needCaptionUpdate = false;
+
+ @Override
+ public void init() {
+ super.init();
+ rpc = RpcProxy.create(GridLayoutServerRpc.class, this);
+ getLayoutManager().registerDependency(this,
+ getWidget().spacingMeasureElement);
+ }
+
+ @Override
+ public void onUnregister() {
+ VGridLayout layout = getWidget();
+ getLayoutManager().unregisterDependency(this,
+ layout.spacingMeasureElement);
+
+ // Unregister caption size dependencies
+ for (ComponentConnector child : getChildComponents()) {
+ Cell cell = layout.widgetToCell.get(child.getWidget());
+ cell.slot.setCaption(null);
+ }
+ }
+
+ @Override
+ public GridLayoutState getState() {
+ return (GridLayoutState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ VGridLayout layout = getWidget();
+ layout.client = client;
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ int cols = getState().getColumns();
+ int rows = getState().getRows();
+
+ layout.columnWidths = new int[cols];
+ layout.rowHeights = new int[rows];
+
+ layout.setSize(rows, cols);
+
+ final int[] alignments = uidl.getIntArrayAttribute("alignments");
+ int alignmentIndex = 0;
+
+ for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) {
+ final UIDL r = (UIDL) i.next();
+ if ("gr".equals(r.getTag())) {
+ for (final Iterator<?> j = r.getChildIterator(); j.hasNext();) {
+ final UIDL cellUidl = (UIDL) j.next();
+ if ("gc".equals(cellUidl.getTag())) {
+ int row = cellUidl.getIntAttribute("y");
+ int col = cellUidl.getIntAttribute("x");
+
+ Widget previousWidget = null;
+
+ Cell cell = layout.getCell(row, col);
+ if (cell != null && cell.slot != null) {
+ // This is an update. Track if the widget changes
+ // and update the caption if that happens. This
+ // workaround can be removed once the DOM update is
+ // done in onContainerHierarchyChange
+ previousWidget = cell.slot.getWidget();
+ }
+
+ cell = layout.createCell(row, col);
+
+ cell.updateFromUidl(cellUidl);
+
+ if (cell.hasContent()) {
+ cell.setAlignment(new AlignmentInfo(
+ alignments[alignmentIndex++]));
+ if (cell.slot.getWidget() != previousWidget) {
+ // Widget changed or widget moved from another
+ // slot. Update its caption as the widget might
+ // have called updateCaption when the widget was
+ // still in its old slot. This workaround can be
+ // removed once the DOM update
+ // is done in onContainerHierarchyChange
+ updateCaption(ConnectorMap.get(getConnection())
+ .getConnector(cell.slot.getWidget()));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ layout.colExpandRatioArray = uidl.getIntArrayAttribute("colExpand");
+ layout.rowExpandRatioArray = uidl.getIntArrayAttribute("rowExpand");
+
+ layout.updateMarginStyleNames(new VMarginInfo(getState()
+ .getMarginsBitmask()));
+
+ layout.updateSpacingStyleName(getState().isSpacing());
+
+ if (needCaptionUpdate) {
+ needCaptionUpdate = false;
+
+ for (ComponentConnector child : getChildComponents()) {
+ updateCaption(child);
+ }
+ }
+ getLayoutManager().setNeedsLayout(this);
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ VGridLayout layout = getWidget();
+
+ // clean non rendered components
+ for (ComponentConnector oldChild : event.getOldChildren()) {
+ if (oldChild.getParent() == this) {
+ continue;
+ }
+
+ Widget childWidget = oldChild.getWidget();
+ layout.remove(childWidget);
+
+ Cell cell = layout.widgetToCell.remove(childWidget);
+ cell.slot.setCaption(null);
+ cell.slot.getWrapperElement().removeFromParent();
+ cell.slot = null;
+ }
+
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector childConnector) {
+ if (!childConnector.delegateCaptionHandling()) {
+ // Check not required by interface but by workarounds in this class
+ // when updateCaption is explicitly called for all children.
+ return;
+ }
+
+ VGridLayout layout = getWidget();
+ Cell cell = layout.widgetToCell.get(childConnector.getWidget());
+ if (cell == null) {
+ // workaround before updateFromUidl is removed. We currently update
+ // the captions at the end of updateFromUidl instead of immediately
+ // because the DOM has not been set up at this point (as it is done
+ // in updateFromUidl)
+ needCaptionUpdate = true;
+ return;
+ }
+ if (VCaption.isNeeded(childConnector.getState())) {
+ VLayoutSlot layoutSlot = cell.slot;
+ VCaption caption = layoutSlot.getCaption();
+ if (caption == null) {
+ caption = new VCaption(childConnector, getConnection());
+
+ Widget widget = childConnector.getWidget();
+
+ layout.setCaption(widget, caption);
+ }
+ caption.updateCaption();
+ } else {
+ layout.setCaption(childConnector.getWidget(), null);
+ }
+ }
+
+ @Override
+ public VGridLayout getWidget() {
+ return (VGridLayout) super.getWidget();
+ }
+
+ @Override
+ public void layoutVertically() {
+ getWidget().updateHeight();
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ getWidget().updateWidth();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java
new file mode 100644
index 0000000000..1ea84d46cd
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/gridlayout/VGridLayout.java
@@ -0,0 +1,702 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.gridlayout;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+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.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+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.shared.ui.AlignmentInfo;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.layout.ComponentConnectorLayoutSlot;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+
+public class VGridLayout extends ComplexPanel {
+
+ public static final String CLASSNAME = "v-gridlayout";
+
+ ApplicationConnection client;
+
+ HashMap<Widget, Cell> widgetToCell = new HashMap<Widget, Cell>();
+
+ int[] columnWidths;
+ int[] rowHeights;
+
+ int[] colExpandRatioArray;
+
+ int[] rowExpandRatioArray;
+
+ int[] minColumnWidths;
+
+ private int[] minRowHeights;
+
+ DivElement spacingMeasureElement;
+
+ public VGridLayout() {
+ super();
+ setElement(Document.get().createDivElement());
+
+ spacingMeasureElement = Document.get().createDivElement();
+ Style spacingStyle = spacingMeasureElement.getStyle();
+ spacingStyle.setPosition(Position.ABSOLUTE);
+ getElement().appendChild(spacingMeasureElement);
+
+ setStyleName(CLASSNAME);
+ }
+
+ private GridLayoutConnector getConnector() {
+ return (GridLayoutConnector) ConnectorMap.get(client)
+ .getConnector(this);
+ }
+
+ /**
+ * Returns the column widths measured in pixels
+ *
+ * @return
+ */
+ protected int[] getColumnWidths() {
+ return columnWidths;
+ }
+
+ /**
+ * Returns the row heights measured in pixels
+ *
+ * @return
+ */
+ protected int[] getRowHeights() {
+ return rowHeights;
+ }
+
+ /**
+ * Returns the spacing between the cells horizontally in pixels
+ *
+ * @return
+ */
+ protected int getHorizontalSpacing() {
+ return LayoutManager.get(client).getOuterWidth(spacingMeasureElement);
+ }
+
+ /**
+ * Returns the spacing between the cells vertically in pixels
+ *
+ * @return
+ */
+ protected int getVerticalSpacing() {
+ return LayoutManager.get(client).getOuterHeight(spacingMeasureElement);
+ }
+
+ 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;
+ }
+
+ void expandRows() {
+ if (!isUndefinedHeight()) {
+ int usedSpace = minRowHeights[0];
+ int verticalSpacing = getVerticalSpacing();
+ for (int i = 1; i < minRowHeights.length; i++) {
+ usedSpace += verticalSpacing + minRowHeights[i];
+ }
+ int availableSpace = LayoutManager.get(client).getInnerHeight(
+ getElement());
+ 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++;
+ }
+ }
+ }
+ }
+
+ void updateHeight() {
+ // Detect minimum heights & calculate spans
+ detectRowHeights();
+
+ // Expand
+ expandRows();
+
+ // Position
+ layoutCellsVertically();
+ }
+
+ void updateWidth() {
+ // Detect widths & calculate spans
+ detectColWidths();
+ // Expand
+ expandColumns();
+ // Position
+ layoutCellsHorizontally();
+
+ }
+
+ void expandColumns() {
+ if (!isUndefinedWidth()) {
+ int usedSpace = minColumnWidths[0];
+ int horizontalSpacing = getHorizontalSpacing();
+ for (int i = 1; i < minColumnWidths.length; i++) {
+ usedSpace += horizontalSpacing + minColumnWidths[i];
+ }
+
+ int availableSpace = LayoutManager.get(client).getInnerWidth(
+ getElement());
+ 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++;
+ }
+ }
+ }
+ }
+
+ void layoutCellsVertically() {
+ int verticalSpacing = getVerticalSpacing();
+ LayoutManager layoutManager = LayoutManager.get(client);
+ Element element = getElement();
+ int paddingTop = layoutManager.getPaddingTop(element);
+ int paddingBottom = layoutManager.getPaddingBottom(element);
+ int y = paddingTop;
+
+ for (int i = 0; i < cells.length; i++) {
+ y = paddingTop;
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ int reservedMargin;
+ if (cell.rowspan + j >= cells[i].length) {
+ // Make room for layout padding for cells reaching the
+ // bottom of the layout
+ reservedMargin = paddingBottom;
+ } else {
+ reservedMargin = 0;
+ }
+ cell.layoutVertically(y, reservedMargin);
+ }
+ y += rowHeights[j] + verticalSpacing;
+ }
+ }
+
+ if (isUndefinedHeight()) {
+ int outerHeight = y - verticalSpacing
+ + layoutManager.getPaddingBottom(element)
+ + layoutManager.getBorderHeight(element);
+ element.getStyle().setHeight(outerHeight, Unit.PX);
+ getConnector().getLayoutManager().reportOuterHeight(getConnector(),
+ outerHeight);
+ }
+ }
+
+ void layoutCellsHorizontally() {
+ LayoutManager layoutManager = LayoutManager.get(client);
+ Element element = getElement();
+ int x = layoutManager.getPaddingLeft(element);
+ int paddingRight = layoutManager.getPaddingRight(element);
+ int horizontalSpacing = getHorizontalSpacing();
+ 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) {
+ int reservedMargin;
+ // Make room for layout padding for cells reaching the
+ // right edge of the layout
+ if (i + cell.colspan >= cells.length) {
+ reservedMargin = paddingRight;
+ } else {
+ reservedMargin = 0;
+ }
+ cell.layoutHorizontally(x, reservedMargin);
+ }
+ }
+ x += columnWidths[i] + horizontalSpacing;
+ }
+
+ if (isUndefinedWidth()) {
+ int outerWidth = x - horizontalSpacing
+ + layoutManager.getPaddingRight(element)
+ + layoutManager.getBorderWidth(element);
+ element.getStyle().setWidth(outerWidth, Unit.PX);
+ getConnector().getLayoutManager().reportOuterWidth(getConnector(),
+ outerWidth);
+ }
+ }
+
+ private boolean isUndefinedHeight() {
+ return getConnector().isUndefinedHeight();
+ }
+
+ private boolean isUndefinedWidth() {
+ return getConnector().isUndefinedWidth();
+ }
+
+ private void detectRowHeights() {
+ for (int i = 0; i < rowHeights.length; i++) {
+ rowHeights[i] = 0;
+ }
+
+ // 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) {
+ if (cell.rowspan == 1) {
+ if (!cell.hasRelativeHeight()
+ && rowHeights[j] < cell.getHeight()) {
+ rowHeights[j] = cell.getHeight();
+ }
+ } else {
+ storeRowSpannedCell(cell);
+ }
+ }
+ }
+ }
+
+ distributeRowSpanHeights();
+
+ minRowHeights = cloneArray(rowHeights);
+ }
+
+ private void detectColWidths() {
+ // collect min colwidths from non-colspanned cells
+ for (int i = 0; i < columnWidths.length; i++) {
+ columnWidths[i] = 0;
+ }
+
+ 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) {
+ if (cell.colspan == 1) {
+ if (!cell.hasRelativeWidth()
+ && columnWidths[i] < cell.getWidth()) {
+ columnWidths[i] = cell.getWidth();
+ }
+ } else {
+ storeColSpannedCell(cell);
+ }
+ }
+ }
+ }
+
+ distributeColSpanWidths();
+
+ minColumnWidths = cloneArray(columnWidths);
+ }
+
+ 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);
+ }
+
+ /**
+ * Iterates colspanned cells, ensures cols have enough space to accommodate
+ * them
+ */
+ void distributeColSpanWidths() {
+ for (SpanList list : colSpans) {
+ for (Cell cell : list.cells) {
+ // cells with relative content may return non 0 here if on
+ // subsequent renders
+ int width = cell.hasRelativeWidth() ? 0 : cell.getWidth();
+ distributeSpanSize(columnWidths, cell.col, cell.colspan,
+ getHorizontalSpacing(), width, colExpandRatioArray);
+ }
+ }
+ }
+
+ /**
+ * Iterates rowspanned cells, ensures rows have enough space to accommodate
+ * them
+ */
+ private void distributeRowSpanHeights() {
+ for (SpanList list : rowSpans) {
+ for (Cell cell : list.cells) {
+ // cells with relative content may return non 0 here if on
+ // subsequent renders
+ int height = cell.hasRelativeHeight() ? 0 : cell.getHeight();
+ distributeSpanSize(rowHeights, cell.row, cell.rowspan,
+ getVerticalSpacing(), height, rowExpandRatioArray);
+ }
+ }
+ }
+
+ private static void distributeSpanSize(int[] dimensions,
+ int spanStartIndex, int spanSize, int spacingSize, int size,
+ int[] expansionRatios) {
+ int allocated = dimensions[spanStartIndex];
+ for (int i = 1; i < spanSize; i++) {
+ allocated += spacingSize + dimensions[spanStartIndex + i];
+ }
+ if (allocated < size) {
+ // dimensions needs to be expanded due spanned cell
+ int neededExtraSpace = size - allocated;
+ int allocatedExtraSpace = 0;
+
+ // Divide space according to expansion ratios if any span has a
+ // ratio
+ int totalExpansion = 0;
+ for (int i = 0; i < spanSize; i++) {
+ int itemIndex = spanStartIndex + i;
+ totalExpansion += expansionRatios[itemIndex];
+ }
+
+ for (int i = 0; i < spanSize; i++) {
+ int itemIndex = spanStartIndex + i;
+ int expansion;
+ if (totalExpansion == 0) {
+ // Divide equally among all cells if there are no
+ // expansion ratios
+ expansion = neededExtraSpace / spanSize;
+ } else {
+ expansion = neededExtraSpace * expansionRatios[itemIndex]
+ / totalExpansion;
+ }
+ dimensions[itemIndex] += expansion;
+ allocatedExtraSpace += expansion;
+ }
+
+ // We might still miss a couple of pixels because of
+ // rounding errors...
+ if (neededExtraSpace > allocatedExtraSpace) {
+ for (int i = 0; i < spanSize; i++) {
+ // Add one pixel to every cell until we have
+ // compensated for any rounding error
+ int itemIndex = spanStartIndex + i;
+ dimensions[itemIndex] += 1;
+ allocatedExtraSpace += 1;
+ if (neededExtraSpace == allocatedExtraSpace) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private LinkedList<SpanList> colSpans = new LinkedList<SpanList>();
+ private LinkedList<SpanList> rowSpans = new LinkedList<SpanList>();
+
+ private class SpanList {
+ final int span;
+ List<Cell> cells = new LinkedList<Cell>();
+
+ public SpanList(int span) {
+ this.span = span;
+ }
+ }
+
+ 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);
+ }
+
+ Cell[][] cells;
+
+ /**
+ * Private helper class.
+ */
+ class Cell {
+ public Cell(int row, int col) {
+ this.row = row;
+ this.col = col;
+ }
+
+ public boolean hasContent() {
+ return hasContent;
+ }
+
+ public boolean hasRelativeHeight() {
+ if (slot != null) {
+ return slot.getChild().isRelativeHeight();
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @return total of spanned cols
+ */
+ private int getAvailableWidth() {
+ int width = columnWidths[col];
+ for (int i = 1; i < colspan; i++) {
+ width += getHorizontalSpacing() + 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 += getVerticalSpacing() + rowHeights[row + i];
+ }
+ return height;
+ }
+
+ public void layoutHorizontally(int x, int marginRight) {
+ if (slot != null) {
+ slot.positionHorizontally(x, getAvailableWidth(), marginRight);
+ }
+ }
+
+ public void layoutVertically(int y, int marginBottom) {
+ if (slot != null) {
+ slot.positionVertically(y, getAvailableHeight(), marginBottom);
+ }
+ }
+
+ public int getWidth() {
+ if (slot != null) {
+ return slot.getUsedWidth();
+ } else {
+ return 0;
+ }
+ }
+
+ public int getHeight() {
+ if (slot != null) {
+ return slot.getUsedHeight();
+ } else {
+ return 0;
+ }
+ }
+
+ protected boolean hasRelativeWidth() {
+ if (slot != null) {
+ return slot.getChild().isRelativeWidth();
+ } else {
+ return true;
+ }
+ }
+
+ final int row;
+ final int col;
+ int colspan = 1;
+ int rowspan = 1;
+
+ private boolean hasContent;
+
+ private AlignmentInfo alignment;
+
+ ComponentConnectorLayoutSlot slot;
+
+ public void updateFromUidl(UIDL cellUidl) {
+ // Set cell width
+ colspan = cellUidl.hasAttribute("w") ? cellUidl
+ .getIntAttribute("w") : 1;
+ // Set cell height
+ rowspan = cellUidl.hasAttribute("h") ? cellUidl
+ .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;
+ }
+ }
+ }
+
+ UIDL childUidl = cellUidl.getChildUIDL(0); // we are interested
+ // about childUidl
+ hasContent = childUidl != null;
+ if (hasContent) {
+ ComponentConnector childConnector = client
+ .getPaintable(childUidl);
+
+ if (slot == null || slot.getChild() != childConnector) {
+ slot = new ComponentConnectorLayoutSlot(CLASSNAME,
+ childConnector, getConnector());
+ if (childConnector.isRelativeWidth()) {
+ slot.getWrapperElement().getStyle()
+ .setWidth(100, Unit.PCT);
+ }
+ Element slotWrapper = slot.getWrapperElement();
+ getElement().appendChild(slotWrapper);
+
+ Widget widget = childConnector.getWidget();
+ insert(widget, slotWrapper, getWidgetCount(), false);
+ Cell oldCell = widgetToCell.put(widget, this);
+ if (oldCell != null) {
+ oldCell.slot.getWrapperElement().removeFromParent();
+ oldCell.slot = null;
+ }
+ }
+
+ }
+ }
+
+ public void setAlignment(AlignmentInfo alignmentInfo) {
+ slot.setAlignment(alignmentInfo);
+ }
+ }
+
+ Cell getCell(int row, int col) {
+ return cells[col][row];
+ }
+
+ /**
+ * Creates a new Cell with the given coordinates. If an existing cell was
+ * found, returns that one.
+ *
+ * @param row
+ * @param col
+ * @return
+ */
+ Cell createCell(int row, int col) {
+ Cell cell = getCell(row, col);
+ if (cell == null) {
+ cell = new Cell(row, col);
+ cells[col][row] = cell;
+ }
+ return cell;
+ }
+
+ /**
+ * Returns the deepest nested child component which contains "element". The
+ * child component is also returned if "element" is part of its caption.
+ *
+ * @param element
+ * An element that is a nested sub element of the root element in
+ * this layout
+ * @return The Paintable which the element is a part of. Null if the element
+ * belongs to the layout and not to a child.
+ */
+ ComponentConnector getComponent(Element element) {
+ return Util.getConnectorForElement(client, this, element);
+ }
+
+ void setCaption(Widget widget, VCaption caption) {
+ VLayoutSlot slot = widgetToCell.get(widget).slot;
+
+ if (caption != null) {
+ // Logical attach.
+ getChildren().add(caption);
+ }
+
+ // Physical attach if not null, also removes old caption
+ slot.setCaption(caption);
+
+ if (caption != null) {
+ // Adopt.
+ adopt(caption);
+ }
+ }
+
+ private void togglePrefixedStyleName(String name, boolean enabled) {
+ if (enabled) {
+ addStyleDependentName(name);
+ } else {
+ removeStyleDependentName(name);
+ }
+ }
+
+ void updateMarginStyleNames(VMarginInfo marginInfo) {
+ togglePrefixedStyleName("margin-top", marginInfo.hasTop());
+ togglePrefixedStyleName("margin-right", marginInfo.hasRight());
+ togglePrefixedStyleName("margin-bottom", marginInfo.hasBottom());
+ togglePrefixedStyleName("margin-left", marginInfo.hasLeft());
+ }
+
+ void updateSpacingStyleName(boolean spacingEnabled) {
+ String styleName = getStylePrimaryName();
+ if (spacingEnabled) {
+ spacingMeasureElement.addClassName(styleName + "-spacing-on");
+ spacingMeasureElement.removeClassName(styleName + "-spacing-off");
+ } else {
+ spacingMeasureElement.removeClassName(styleName + "-spacing-on");
+ spacingMeasureElement.addClassName(styleName + "-spacing-off");
+ }
+ }
+
+ public void setSize(int rows, int cols) {
+ 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;
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java
new file mode 100644
index 0000000000..4c6c71e037
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/label/LabelConnector.java
@@ -0,0 +1,69 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.label;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.PreElement;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.label.LabelState;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.ui.Label;
+
+@Connect(value = Label.class, loadStyle = LoadStyle.EAGER)
+public class LabelConnector extends AbstractComponentConnector {
+
+ @Override
+ public LabelState getState() {
+ return (LabelState) super.getState();
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().setConnection(getConnection());
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ boolean sinkOnloads = false;
+ switch (getState().getContentMode()) {
+ case PREFORMATTED:
+ PreElement preElement = Document.get().createPreElement();
+ preElement.setInnerText(getState().getText());
+ // clear existing content
+ getWidget().setHTML("");
+ // add preformatted text to dom
+ getWidget().getElement().appendChild(preElement);
+ break;
+
+ case TEXT:
+ getWidget().setText(getState().getText());
+ break;
+
+ case XHTML:
+ case RAW:
+ sinkOnloads = true;
+ case XML:
+ getWidget().setHTML(getState().getText());
+ break;
+ default:
+ getWidget().setText("");
+ break;
+
+ }
+ if (sinkOnloads) {
+ Util.sinkOnloadForImages(getWidget().getElement());
+ }
+ }
+
+ @Override
+ public VLabel getWidget() {
+ return (VLabel) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java b/client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java
new file mode 100644
index 0000000000..f0c170c6b0
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/label/VLabel.java
@@ -0,0 +1,69 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.label;
+
+import com.google.gwt.dom.client.Style.Display;
+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.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VTooltip;
+
+public class VLabel extends HTML {
+
+ public static final String CLASSNAME = "v-label";
+ private static final String CLASSNAME_UNDEFINED_WIDTH = "v-label-undef-w";
+
+ private ApplicationConnection connection;
+
+ public VLabel() {
+ super();
+ setStyleName(CLASSNAME);
+ sinkEvents(VTooltip.TOOLTIP_EVENTS);
+ }
+
+ public VLabel(String text) {
+ super(text);
+ setStyleName(CLASSNAME);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ event.stopPropagation();
+ return;
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (width == null || width.equals("")) {
+ setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, true);
+ getElement().getStyle().setDisplay(Display.INLINE_BLOCK);
+ } else {
+ setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, false);
+ getElement().getStyle().clearDisplay();
+ }
+ }
+
+ @Override
+ public void setText(String text) {
+ if (BrowserInfo.get().isIE8()) {
+ // #3983 - IE8 incorrectly replaces \n with <br> so we do the
+ // escaping manually and set as HTML
+ super.setHTML(Util.escapeHTML(text));
+ } else {
+ super.setText(text);
+ }
+ }
+
+ void setConnection(ApplicationConnection client) {
+ connection = client;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java
new file mode 100644
index 0000000000..d479e8da9d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ComponentConnectorLayoutSlot.java
@@ -0,0 +1,99 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.ManagedLayout;
+
+public class ComponentConnectorLayoutSlot extends VLayoutSlot {
+
+ final ComponentConnector child;
+ final ManagedLayout layout;
+
+ public ComponentConnectorLayoutSlot(String baseClassName,
+ ComponentConnector child, ManagedLayout layout) {
+ super(baseClassName, child.getWidget());
+ this.child = child;
+ this.layout = layout;
+ }
+
+ public ComponentConnector getChild() {
+ return child;
+ }
+
+ @Override
+ protected int getCaptionHeight() {
+ VCaption caption = getCaption();
+ return caption != null ? getLayoutManager().getOuterHeight(
+ caption.getElement()) : 0;
+ }
+
+ @Override
+ protected int getCaptionWidth() {
+ VCaption caption = getCaption();
+ return caption != null ? getLayoutManager().getOuterWidth(
+ caption.getElement()) : 0;
+ }
+
+ public LayoutManager getLayoutManager() {
+ return layout.getLayoutManager();
+ }
+
+ @Override
+ public void setCaption(VCaption caption) {
+ VCaption oldCaption = getCaption();
+ if (oldCaption != null) {
+ getLayoutManager().unregisterDependency(layout,
+ oldCaption.getElement());
+ }
+ super.setCaption(caption);
+ if (caption != null) {
+ getLayoutManager().registerDependency(
+ (ManagedLayout) child.getParent(), caption.getElement());
+ }
+ }
+
+ @Override
+ protected void reportActualRelativeHeight(int allocatedHeight) {
+ getLayoutManager().reportOuterHeight(child, allocatedHeight);
+ }
+
+ @Override
+ protected void reportActualRelativeWidth(int allocatedWidth) {
+ getLayoutManager().reportOuterWidth(child, allocatedWidth);
+ }
+
+ @Override
+ public int getWidgetHeight() {
+ return getLayoutManager()
+ .getOuterHeight(child.getWidget().getElement());
+ }
+
+ @Override
+ public int getWidgetWidth() {
+ return getLayoutManager().getOuterWidth(child.getWidget().getElement());
+ }
+
+ @Override
+ public boolean isUndefinedHeight() {
+ return child.isUndefinedHeight();
+ }
+
+ @Override
+ public boolean isUndefinedWidth() {
+ return child.isUndefinedWidth();
+ }
+
+ @Override
+ public boolean isRelativeHeight() {
+ return child.isRelativeHeight();
+ }
+
+ @Override
+ public boolean isRelativeWidth() {
+ return child.isRelativeWidth();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java
new file mode 100644
index 0000000000..a519f5db87
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeEvent.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import com.google.gwt.dom.client.Element;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+
+public class ElementResizeEvent {
+ private final Element element;
+ private final LayoutManager layoutManager;
+
+ public ElementResizeEvent(LayoutManager layoutManager, Element element) {
+ this.layoutManager = layoutManager;
+ this.element = element;
+ }
+
+ public Element getElement() {
+ return element;
+ }
+
+ public LayoutManager getLayoutManager() {
+ return layoutManager;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java
new file mode 100644
index 0000000000..d6d3de48b8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/ElementResizeListener.java
@@ -0,0 +1,9 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+public interface ElementResizeListener {
+ public void onElementResize(ElementResizeEvent e);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java
new file mode 100644
index 0000000000..cb0ed697c9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/LayoutDependencyTree.java
@@ -0,0 +1,520 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.shared.ComponentState;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.ManagedLayout;
+
+public class LayoutDependencyTree {
+ private class LayoutDependency {
+ private final ComponentConnector connector;
+ private final int direction;
+
+ private boolean needsLayout = false;
+ private boolean needsMeasure = false;
+
+ private boolean scrollingParentCached = false;
+ private ComponentConnector scrollingBoundary = null;
+
+ private Set<ComponentConnector> measureBlockers = new HashSet<ComponentConnector>();
+ private Set<ComponentConnector> layoutBlockers = new HashSet<ComponentConnector>();
+
+ public LayoutDependency(ComponentConnector connector, int direction) {
+ this.connector = connector;
+ this.direction = direction;
+ }
+
+ private void addLayoutBlocker(ComponentConnector blocker) {
+ boolean blockerAdded = layoutBlockers.add(blocker);
+ if (blockerAdded && layoutBlockers.size() == 1) {
+ if (needsLayout) {
+ getLayoutQueue(direction).remove(connector);
+ } else {
+ // Propagation already done if needsLayout is set
+ propagatePotentialLayout();
+ }
+ }
+ }
+
+ private void removeLayoutBlocker(ComponentConnector blocker) {
+ boolean removed = layoutBlockers.remove(blocker);
+ if (removed && layoutBlockers.isEmpty()) {
+ if (needsLayout) {
+ getLayoutQueue(direction).add((ManagedLayout) connector);
+ } else {
+ propagateNoUpcomingLayout();
+ }
+ }
+ }
+
+ private void addMeasureBlocker(ComponentConnector blocker) {
+ boolean blockerAdded = measureBlockers.add(blocker);
+ if (blockerAdded && measureBlockers.size() == 1) {
+ if (needsMeasure) {
+ getMeasureQueue(direction).remove(connector);
+ } else {
+ propagatePotentialResize();
+ }
+ }
+ }
+
+ private void removeMeasureBlocker(ComponentConnector blocker) {
+ boolean removed = measureBlockers.remove(blocker);
+ if (removed && measureBlockers.isEmpty()) {
+ if (needsMeasure) {
+ getMeasureQueue(direction).add(connector);
+ } else {
+ propagateNoUpcomingResize();
+ }
+ }
+ }
+
+ public void setNeedsMeasure(boolean needsMeasure) {
+ if (needsMeasure && !this.needsMeasure) {
+ // If enabling needsMeasure
+ this.needsMeasure = needsMeasure;
+
+ if (measureBlockers.isEmpty()) {
+ // Add to queue if there are no blockers
+ getMeasureQueue(direction).add(connector);
+ // Only need to propagate if not already propagated when
+ // setting blockers
+ propagatePotentialResize();
+ }
+ } else if (!needsMeasure && this.needsMeasure
+ && measureBlockers.isEmpty()) {
+ // Only disable if there are no blockers (elements gets measured
+ // in both directions even if there is a blocker in one
+ // direction)
+ this.needsMeasure = needsMeasure;
+ getMeasureQueue(direction).remove(connector);
+ propagateNoUpcomingResize();
+ }
+ }
+
+ public void setNeedsLayout(boolean needsLayout) {
+ if (!(connector instanceof ManagedLayout)) {
+ throw new IllegalStateException(
+ "Only managed layouts can need layout, layout attempted for "
+ + Util.getConnectorString(connector));
+ }
+ if (needsLayout && !this.needsLayout) {
+ // If enabling needsLayout
+ this.needsLayout = needsLayout;
+
+ if (layoutBlockers.isEmpty()) {
+ // Add to queue if there are no blockers
+ getLayoutQueue(direction).add((ManagedLayout) connector);
+ // Only need to propagate if not already propagated when
+ // setting blockers
+ propagatePotentialLayout();
+ }
+ } else if (!needsLayout && this.needsLayout
+ && layoutBlockers.isEmpty()) {
+ // Only disable if there are no layout blockers
+ // (SimpleManagedLayout gets layouted in both directions
+ // even if there is a blocker in one direction)
+ this.needsLayout = needsLayout;
+ getLayoutQueue(direction).remove(connector);
+ propagateNoUpcomingLayout();
+ }
+ }
+
+ private void propagatePotentialResize() {
+ for (ComponentConnector needsSize : getNeedsSizeForLayout()) {
+ LayoutDependency layoutDependency = getDependency(needsSize,
+ direction);
+ layoutDependency.addLayoutBlocker(connector);
+ }
+ }
+
+ private Collection<ComponentConnector> getNeedsSizeForLayout() {
+ // Find all connectors that need the size of this connector for
+ // layouting
+
+ // Parent needs size if it isn't relative?
+ // Connector itself needs size if it isn't undefined?
+ // Children doesn't care?
+
+ ArrayList<ComponentConnector> needsSize = new ArrayList<ComponentConnector>();
+
+ if (!isUndefinedInDirection(connector, direction)) {
+ needsSize.add(connector);
+ }
+ if (!isRelativeInDirection(connector, direction)) {
+ ServerConnector parent = connector.getParent();
+ if (parent instanceof ComponentConnector) {
+ needsSize.add((ComponentConnector) parent);
+ }
+ }
+
+ return needsSize;
+ }
+
+ private void propagateNoUpcomingResize() {
+ for (ComponentConnector mightNeedLayout : getNeedsSizeForLayout()) {
+ LayoutDependency layoutDependency = getDependency(
+ mightNeedLayout, direction);
+ layoutDependency.removeLayoutBlocker(connector);
+ }
+ }
+
+ private void propagatePotentialLayout() {
+ for (ComponentConnector sizeMightChange : getResizedByLayout()) {
+ LayoutDependency layoutDependency = getDependency(
+ sizeMightChange, direction);
+ layoutDependency.addMeasureBlocker(connector);
+ }
+ }
+
+ private Collection<ComponentConnector> getResizedByLayout() {
+ // Components that might get resized by a layout of this component
+
+ // Parent never resized
+ // Connector itself resized if undefined
+ // Children resized if relative
+
+ ArrayList<ComponentConnector> resized = new ArrayList<ComponentConnector>();
+ if (isUndefinedInDirection(connector, direction)) {
+ resized.add(connector);
+ }
+
+ if (connector instanceof ComponentContainerConnector) {
+ ComponentContainerConnector container = (ComponentContainerConnector) connector;
+ for (ComponentConnector child : container.getChildComponents()) {
+ if (isRelativeInDirection(child, direction)) {
+ resized.add(child);
+ }
+ }
+ }
+
+ return resized;
+ }
+
+ private void propagateNoUpcomingLayout() {
+ for (ComponentConnector sizeMightChange : getResizedByLayout()) {
+ LayoutDependency layoutDependency = getDependency(
+ sizeMightChange, direction);
+ layoutDependency.removeMeasureBlocker(connector);
+ }
+ }
+
+ public void markSizeAsChanged() {
+ // When the size has changed, all that use that size should be
+ // layouted
+ for (ComponentConnector connector : getNeedsSizeForLayout()) {
+ LayoutDependency layoutDependency = getDependency(connector,
+ direction);
+ if (connector instanceof ManagedLayout) {
+ layoutDependency.setNeedsLayout(true);
+ } else {
+ // Should simulate setNeedsLayout(true) + markAsLayouted ->
+ // propagate needs measure
+ layoutDependency.propagatePostLayoutMeasure();
+ }
+ }
+
+ // Should also go through the hierarchy to discover appeared or
+ // disappeared scrollbars
+ ComponentConnector scrollingBoundary = getScrollingBoundary(connector);
+ if (scrollingBoundary != null) {
+ getDependency(scrollingBoundary, getOppositeDirection())
+ .setNeedsMeasure(true);
+ }
+
+ }
+
+ /**
+ * Go up the hierarchy to find a component whose size might have changed
+ * in the other direction because changes to this component causes
+ * scrollbars to appear or disappear.
+ *
+ * @return
+ */
+ private LayoutDependency findPotentiallyChangedScrollbar() {
+ ComponentConnector currentConnector = connector;
+ while (true) {
+ ServerConnector parent = currentConnector.getParent();
+ if (!(parent instanceof ComponentConnector)) {
+ return null;
+ }
+ if (parent instanceof MayScrollChildren) {
+ return getDependency(currentConnector,
+ getOppositeDirection());
+ }
+ currentConnector = (ComponentConnector) parent;
+ }
+ }
+
+ private int getOppositeDirection() {
+ return direction == HORIZONTAL ? VERTICAL : HORIZONTAL;
+ }
+
+ public void markAsLayouted() {
+ if (!layoutBlockers.isEmpty()) {
+ // Don't do anything if there are layout blockers (SimpleLayout
+ // gets layouted in both directions even if one direction is
+ // blocked)
+ return;
+ }
+ setNeedsLayout(false);
+ propagatePostLayoutMeasure();
+ }
+
+ private void propagatePostLayoutMeasure() {
+ for (ComponentConnector resized : getResizedByLayout()) {
+ LayoutDependency layoutDependency = getDependency(resized,
+ direction);
+ layoutDependency.setNeedsMeasure(true);
+ }
+
+ // Special case for e.g. wrapping texts
+ if (direction == HORIZONTAL && !connector.isUndefinedWidth()
+ && connector.isUndefinedHeight()) {
+ LayoutDependency dependency = getDependency(connector, VERTICAL);
+ dependency.setNeedsMeasure(true);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String s = getCompactConnectorString(connector) + "\n";
+ if (direction == VERTICAL) {
+ s += "Vertical";
+ } else {
+ s += "Horizontal";
+ }
+ ComponentState state = connector.getState();
+ s += " sizing: "
+ + getSizeDefinition(direction == VERTICAL ? state
+ .getHeight() : state.getWidth()) + "\n";
+
+ if (needsLayout) {
+ s += "Needs layout\n";
+ }
+ if (getLayoutQueue(direction).contains(connector)) {
+ s += "In layout queue\n";
+ }
+ s += "Layout blockers: " + blockersToString(layoutBlockers) + "\n";
+
+ if (needsMeasure) {
+ s += "Needs measure\n";
+ }
+ if (getMeasureQueue(direction).contains(connector)) {
+ s += "In measure queue\n";
+ }
+ s += "Measure blockers: " + blockersToString(measureBlockers);
+
+ return s;
+ }
+
+ public boolean noMoreChangesExpected() {
+ return !needsLayout && !needsMeasure && layoutBlockers.isEmpty()
+ && measureBlockers.isEmpty();
+ }
+
+ }
+
+ private static final int HORIZONTAL = 0;
+ private static final int VERTICAL = 1;
+
+ private final Map<?, ?>[] dependenciesInDirection = new Map<?, ?>[] {
+ new HashMap<ComponentConnector, LayoutDependency>(),
+ new HashMap<ComponentConnector, LayoutDependency>() };
+
+ private final Collection<?>[] measureQueueInDirection = new HashSet<?>[] {
+ new HashSet<ComponentConnector>(),
+ new HashSet<ComponentConnector>() };
+
+ private final Collection<?>[] layoutQueueInDirection = new HashSet<?>[] {
+ new HashSet<ComponentConnector>(),
+ new HashSet<ComponentConnector>() };
+
+ public void setNeedsMeasure(ComponentConnector connector,
+ boolean needsMeasure) {
+ setNeedsHorizontalMeasure(connector, needsMeasure);
+ setNeedsVerticalMeasure(connector, needsMeasure);
+ }
+
+ public void setNeedsHorizontalMeasure(ComponentConnector connector,
+ boolean needsMeasure) {
+ LayoutDependency dependency = getDependency(connector, HORIZONTAL);
+ dependency.setNeedsMeasure(needsMeasure);
+ }
+
+ public void setNeedsVerticalMeasure(ComponentConnector connector,
+ boolean needsMeasure) {
+ LayoutDependency dependency = getDependency(connector, VERTICAL);
+ dependency.setNeedsMeasure(needsMeasure);
+ }
+
+ private LayoutDependency getDependency(ComponentConnector connector,
+ int direction) {
+ @SuppressWarnings("unchecked")
+ Map<ComponentConnector, LayoutDependency> dependencies = (Map<ComponentConnector, LayoutDependency>) dependenciesInDirection[direction];
+ LayoutDependency dependency = dependencies.get(connector);
+ if (dependency == null) {
+ dependency = new LayoutDependency(connector, direction);
+ dependencies.put(connector, dependency);
+ }
+ return dependency;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Collection<ManagedLayout> getLayoutQueue(int direction) {
+ return (Collection<ManagedLayout>) layoutQueueInDirection[direction];
+ }
+
+ @SuppressWarnings("unchecked")
+ private Collection<ComponentConnector> getMeasureQueue(int direction) {
+ return (Collection<ComponentConnector>) measureQueueInDirection[direction];
+ }
+
+ public void setNeedsHorizontalLayout(ManagedLayout layout,
+ boolean needsLayout) {
+ LayoutDependency dependency = getDependency(layout, HORIZONTAL);
+ dependency.setNeedsLayout(needsLayout);
+ }
+
+ public void setNeedsVerticalLayout(ManagedLayout layout, boolean needsLayout) {
+ LayoutDependency dependency = getDependency(layout, VERTICAL);
+ dependency.setNeedsLayout(needsLayout);
+ }
+
+ public void markAsHorizontallyLayouted(ManagedLayout layout) {
+ LayoutDependency dependency = getDependency(layout, HORIZONTAL);
+ dependency.markAsLayouted();
+ }
+
+ public void markAsVerticallyLayouted(ManagedLayout layout) {
+ LayoutDependency dependency = getDependency(layout, VERTICAL);
+ dependency.markAsLayouted();
+ }
+
+ public void markHeightAsChanged(ComponentConnector connector) {
+ LayoutDependency dependency = getDependency(connector, VERTICAL);
+ dependency.markSizeAsChanged();
+ }
+
+ public void markWidthAsChanged(ComponentConnector connector) {
+ LayoutDependency dependency = getDependency(connector, HORIZONTAL);
+ dependency.markSizeAsChanged();
+ }
+
+ private static boolean isRelativeInDirection(ComponentConnector connector,
+ int direction) {
+ if (direction == HORIZONTAL) {
+ return connector.isRelativeWidth();
+ } else {
+ return connector.isRelativeHeight();
+ }
+ }
+
+ private static boolean isUndefinedInDirection(ComponentConnector connector,
+ int direction) {
+ if (direction == VERTICAL) {
+ return connector.isUndefinedHeight();
+ } else {
+ return connector.isUndefinedWidth();
+ }
+ }
+
+ private static String getCompactConnectorString(ComponentConnector connector) {
+ return Util.getSimpleName(connector) + " ("
+ + connector.getConnectorId() + ")";
+ }
+
+ private static String getSizeDefinition(String size) {
+ if (size == null || size.length() == 0) {
+ return "undefined";
+ } else if (size.endsWith("%")) {
+ return "relative";
+ } else {
+ return "fixed";
+ }
+ }
+
+ private static String blockersToString(
+ Collection<ComponentConnector> blockers) {
+ StringBuilder b = new StringBuilder("[");
+ for (ComponentConnector blocker : blockers) {
+ if (b.length() != 1) {
+ b.append(", ");
+ }
+ b.append(getCompactConnectorString(blocker));
+ }
+ b.append(']');
+ return b.toString();
+ }
+
+ public boolean hasConnectorsToMeasure() {
+ return !measureQueueInDirection[HORIZONTAL].isEmpty()
+ || !measureQueueInDirection[VERTICAL].isEmpty();
+ }
+
+ public boolean hasHorizontalConnectorToLayout() {
+ return !getLayoutQueue(HORIZONTAL).isEmpty();
+ }
+
+ public boolean hasVerticaConnectorToLayout() {
+ return !getLayoutQueue(VERTICAL).isEmpty();
+ }
+
+ public ManagedLayout[] getHorizontalLayoutTargets() {
+ Collection<ManagedLayout> queue = getLayoutQueue(HORIZONTAL);
+ return queue.toArray(new ManagedLayout[queue.size()]);
+ }
+
+ public ManagedLayout[] getVerticalLayoutTargets() {
+ Collection<ManagedLayout> queue = getLayoutQueue(VERTICAL);
+ return queue.toArray(new ManagedLayout[queue.size()]);
+ }
+
+ public Collection<ComponentConnector> getMeasureTargets() {
+ Collection<ComponentConnector> measureTargets = new HashSet<ComponentConnector>(
+ getMeasureQueue(HORIZONTAL));
+ measureTargets.addAll(getMeasureQueue(VERTICAL));
+ return measureTargets;
+ }
+
+ public void logDependencyStatus(ComponentConnector connector) {
+ VConsole.log("====");
+ VConsole.log(getDependency(connector, HORIZONTAL).toString());
+ VConsole.log(getDependency(connector, VERTICAL).toString());
+ }
+
+ public boolean noMoreChangesExpected(ComponentConnector connector) {
+ return getDependency(connector, HORIZONTAL).noMoreChangesExpected()
+ && getDependency(connector, VERTICAL).noMoreChangesExpected();
+ }
+
+ public ComponentConnector getScrollingBoundary(ComponentConnector connector) {
+ LayoutDependency dependency = getDependency(connector, HORIZONTAL);
+ if (!dependency.scrollingParentCached) {
+ ServerConnector parent = dependency.connector.getParent();
+ if (parent instanceof MayScrollChildren) {
+ dependency.scrollingBoundary = connector;
+ } else if (parent instanceof ComponentConnector) {
+ dependency.scrollingBoundary = getScrollingBoundary((ComponentConnector) parent);
+ } else {
+ // No scrolling parent
+ }
+
+ dependency.scrollingParentCached = true;
+ }
+ return dependency.scrollingBoundary;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java
new file mode 100644
index 0000000000..37ca7fef37
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java
@@ -0,0 +1,86 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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/client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java
new file mode 100644
index 0000000000..62c9937c4c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/MayScrollChildren.java
@@ -0,0 +1,10 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import com.vaadin.terminal.gwt.client.ComponentContainerConnector;
+
+public interface MayScrollChildren extends ComponentContainerConnector {
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java b/client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java
new file mode 100644
index 0000000000..5b2b1fc49b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/layout/VLayoutSlot.java
@@ -0,0 +1,287 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.AlignmentInfo;
+import com.vaadin.terminal.gwt.client.VCaption;
+
+public abstract class VLayoutSlot {
+
+ private final Element wrapper = Document.get().createDivElement().cast();
+
+ private AlignmentInfo alignment;
+ private VCaption caption;
+ private final Widget widget;
+
+ private double expandRatio;
+
+ public VLayoutSlot(String baseClassName, Widget widget) {
+ this.widget = widget;
+
+ wrapper.setClassName(baseClassName + "-slot");
+ }
+
+ public VCaption getCaption() {
+ return caption;
+ }
+
+ public void setCaption(VCaption caption) {
+ if (this.caption != null) {
+ this.caption.removeFromParent();
+ }
+ this.caption = caption;
+ if (caption != null) {
+ // Physical attach.
+ DOM.insertBefore(wrapper, caption.getElement(), widget.getElement());
+ Style style = caption.getElement().getStyle();
+ style.setPosition(Position.ABSOLUTE);
+ style.setTop(0, Unit.PX);
+ }
+ }
+
+ public AlignmentInfo getAlignment() {
+ return alignment;
+ }
+
+ public Widget getWidget() {
+ return widget;
+ }
+
+ public void setAlignment(AlignmentInfo alignment) {
+ this.alignment = alignment;
+ }
+
+ public void positionHorizontally(double currentLocation,
+ double allocatedSpace, double marginRight) {
+ Style style = wrapper.getStyle();
+
+ double availableWidth = allocatedSpace;
+
+ VCaption caption = getCaption();
+ Style captionStyle = caption != null ? caption.getElement().getStyle()
+ : null;
+ int captionWidth = getCaptionWidth();
+
+ boolean captionAboveCompnent;
+ if (caption == null) {
+ captionAboveCompnent = false;
+ style.clearPaddingRight();
+ } else {
+ captionAboveCompnent = !caption.shouldBePlacedAfterComponent();
+ if (!captionAboveCompnent) {
+ availableWidth -= captionWidth;
+ captionStyle.clearLeft();
+ captionStyle.setRight(0, Unit.PX);
+ style.setPaddingRight(captionWidth, Unit.PX);
+ } else {
+ captionStyle.setLeft(0, Unit.PX);
+ captionStyle.clearRight();
+ style.clearPaddingRight();
+ }
+ }
+
+ if (marginRight > 0) {
+ style.setMarginRight(marginRight, Unit.PX);
+ } else {
+ style.clearMarginRight();
+ }
+
+ if (isRelativeWidth()) {
+ style.setPropertyPx("width", (int) availableWidth);
+ } else {
+ style.clearProperty("width");
+ }
+
+ double allocatedContentWidth = 0;
+ if (isRelativeWidth()) {
+ String percentWidth = getWidget().getElement().getStyle()
+ .getWidth();
+ double percentage = parsePercent(percentWidth);
+ allocatedContentWidth = availableWidth * (percentage / 100);
+ reportActualRelativeWidth(Math.round((float) allocatedContentWidth));
+ }
+
+ AlignmentInfo alignment = getAlignment();
+ if (!alignment.isLeft()) {
+ double usedWidth;
+ if (isRelativeWidth()) {
+ usedWidth = allocatedContentWidth;
+ } else {
+ usedWidth = getWidgetWidth();
+ }
+ if (alignment.isHorizontalCenter()) {
+ currentLocation += (allocatedSpace - usedWidth) / 2d;
+ if (captionAboveCompnent) {
+ captionStyle.setLeft(
+ Math.round(usedWidth - captionWidth) / 2, Unit.PX);
+ }
+ } else {
+ currentLocation += (allocatedSpace - usedWidth);
+ if (captionAboveCompnent) {
+ captionStyle.setLeft(Math.round(usedWidth - captionWidth),
+ Unit.PX);
+ }
+ }
+ } else {
+ if (captionAboveCompnent) {
+ captionStyle.setLeft(0, Unit.PX);
+ }
+ }
+
+ style.setLeft(Math.round(currentLocation), Unit.PX);
+ }
+
+ private double parsePercent(String size) {
+ return Double.parseDouble(size.replaceAll("%", ""));
+ }
+
+ public void positionVertically(double currentLocation,
+ double allocatedSpace, double marginBottom) {
+ Style style = wrapper.getStyle();
+
+ double contentHeight = allocatedSpace;
+
+ int captionHeight;
+ VCaption caption = getCaption();
+ if (caption == null || caption.shouldBePlacedAfterComponent()) {
+ style.clearPaddingTop();
+ captionHeight = 0;
+ } else {
+ captionHeight = getCaptionHeight();
+ contentHeight -= captionHeight;
+ if (contentHeight < 0) {
+ contentHeight = 0;
+ }
+ style.setPaddingTop(captionHeight, Unit.PX);
+ }
+
+ if (marginBottom > 0) {
+ style.setMarginBottom(marginBottom, Unit.PX);
+ } else {
+ style.clearMarginBottom();
+ }
+
+ if (isRelativeHeight()) {
+ style.setHeight(contentHeight, Unit.PX);
+ } else {
+ style.clearHeight();
+ }
+
+ double allocatedContentHeight = 0;
+ if (isRelativeHeight()) {
+ String height = getWidget().getElement().getStyle().getHeight();
+ double percentage = parsePercent(height);
+ allocatedContentHeight = contentHeight * (percentage / 100);
+ reportActualRelativeHeight(Math
+ .round((float) allocatedContentHeight));
+ }
+
+ AlignmentInfo alignment = getAlignment();
+ if (!alignment.isTop()) {
+ double usedHeight;
+ if (isRelativeHeight()) {
+ usedHeight = captionHeight + allocatedContentHeight;
+ } else {
+ usedHeight = getUsedHeight();
+ }
+ if (alignment.isVerticalCenter()) {
+ currentLocation += (allocatedSpace - usedHeight) / 2d;
+ } else {
+ currentLocation += (allocatedSpace - usedHeight);
+ }
+ }
+
+ style.setTop(currentLocation, Unit.PX);
+ }
+
+ protected void reportActualRelativeHeight(int allocatedHeight) {
+ // Default implementation does nothing
+ }
+
+ protected void reportActualRelativeWidth(int allocatedWidth) {
+ // Default implementation does nothing
+ }
+
+ public void positionInDirection(double currentLocation,
+ double allocatedSpace, double endingMargin, boolean isVertical) {
+ if (isVertical) {
+ positionVertically(currentLocation, allocatedSpace, endingMargin);
+ } else {
+ positionHorizontally(currentLocation, allocatedSpace, endingMargin);
+ }
+ }
+
+ public int getWidgetSizeInDirection(boolean isVertical) {
+ return isVertical ? getWidgetHeight() : getWidgetWidth();
+ }
+
+ public int getUsedWidth() {
+ int widgetWidth = getWidgetWidth();
+ if (caption == null) {
+ return widgetWidth;
+ } else if (caption.shouldBePlacedAfterComponent()) {
+ return widgetWidth + getCaptionWidth();
+ } else {
+ return Math.max(widgetWidth, getCaptionWidth());
+ }
+ }
+
+ public int getUsedHeight() {
+ int widgetHeight = getWidgetHeight();
+ if (caption == null) {
+ return widgetHeight;
+ } else if (caption.shouldBePlacedAfterComponent()) {
+ return Math.max(widgetHeight, getCaptionHeight());
+ } else {
+ return widgetHeight + getCaptionHeight();
+ }
+ }
+
+ public int getUsedSizeInDirection(boolean isVertical) {
+ return isVertical ? getUsedHeight() : getUsedWidth();
+ }
+
+ protected abstract int getCaptionHeight();
+
+ protected abstract int getCaptionWidth();
+
+ public abstract int getWidgetHeight();
+
+ public abstract int getWidgetWidth();
+
+ public abstract boolean isUndefinedHeight();
+
+ public abstract boolean isUndefinedWidth();
+
+ public boolean isUndefinedInDirection(boolean isVertical) {
+ return isVertical ? isUndefinedHeight() : isUndefinedWidth();
+ }
+
+ public abstract boolean isRelativeHeight();
+
+ public abstract boolean isRelativeWidth();
+
+ public boolean isRelativeInDirection(boolean isVertical) {
+ return isVertical ? isRelativeHeight() : isRelativeWidth();
+ }
+
+ public Element getWrapperElement() {
+ return wrapper;
+ }
+
+ public void setExpandRatio(double expandRatio) {
+ this.expandRatio = expandRatio;
+ }
+
+ public double getExpandRatio() {
+ return expandRatio;
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java
new file mode 100644
index 0000000000..5ef641470d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/link/LinkConnector.java
@@ -0,0 +1,93 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.link;
+
+import com.google.gwt.user.client.DOM;
+import com.vaadin.shared.ui.Connect;
+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.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.ui.Link;
+
+@Connect(Link.class)
+public class LinkConnector extends AbstractComponentConnector implements
+ Paintable {
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().client = client;
+
+ getWidget().enabled = isEnabled();
+
+ if (uidl.hasAttribute("name")) {
+ getWidget().target = uidl.getStringAttribute("name");
+ getWidget().anchor.setAttribute("target", getWidget().target);
+ }
+ if (uidl.hasAttribute("src")) {
+ getWidget().src = client.translateVaadinUri(uidl
+ .getStringAttribute("src"));
+ getWidget().anchor.setAttribute("href", getWidget().src);
+ }
+
+ if (uidl.hasAttribute("border")) {
+ if ("none".equals(uidl.getStringAttribute("border"))) {
+ getWidget().borderStyle = VLink.BORDER_STYLE_NONE;
+ } else {
+ getWidget().borderStyle = VLink.BORDER_STYLE_MINIMAL;
+ }
+ } else {
+ getWidget().borderStyle = VLink.BORDER_STYLE_DEFAULT;
+ }
+
+ getWidget().targetHeight = uidl.hasAttribute("targetHeight") ? uidl
+ .getIntAttribute("targetHeight") : -1;
+ getWidget().targetWidth = uidl.hasAttribute("targetWidth") ? uidl
+ .getIntAttribute("targetWidth") : -1;
+
+ // Set link caption
+ getWidget().captionElement.setInnerText(getState().getCaption());
+
+ // handle error
+ if (null != getState().getErrorMessage()) {
+ if (getWidget().errorIndicatorElement == null) {
+ getWidget().errorIndicatorElement = DOM.createDiv();
+ DOM.setElementProperty(getWidget().errorIndicatorElement,
+ "className", "v-errorindicator");
+ }
+ DOM.insertChild(getWidget().getElement(),
+ getWidget().errorIndicatorElement, 0);
+ } else if (getWidget().errorIndicatorElement != null) {
+ DOM.setStyleAttribute(getWidget().errorIndicatorElement, "display",
+ "none");
+ }
+
+ if (getState().getIcon() != null) {
+ if (getWidget().icon == null) {
+ getWidget().icon = new Icon(client);
+ getWidget().anchor.insertBefore(getWidget().icon.getElement(),
+ getWidget().captionElement);
+ }
+ getWidget().icon.setUri(getState().getIcon().getURL());
+ }
+
+ }
+
+ @Override
+ public VLink getWidget() {
+ return (VLink) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java b/client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java
new file mode 100644
index 0000000000..4df3220ef1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/link/VLink.java
@@ -0,0 +1,113 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.link;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.HTML;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+
+public class VLink extends HTML implements ClickHandler {
+
+ public static final String CLASSNAME = "v-link";
+
+ protected static final int BORDER_STYLE_DEFAULT = 0;
+ protected static final int BORDER_STYLE_MINIMAL = 1;
+ protected static final int BORDER_STYLE_NONE = 2;
+
+ protected String src;
+
+ protected String target;
+
+ protected int borderStyle = BORDER_STYLE_DEFAULT;
+
+ protected boolean enabled;
+
+ protected int targetWidth;
+
+ protected int targetHeight;
+
+ protected Element errorIndicatorElement;
+
+ protected final Element anchor = DOM.createAnchor();
+
+ protected final Element captionElement = DOM.createSpan();
+
+ protected Icon icon;
+
+ protected ApplicationConnection client;
+
+ public VLink() {
+ super();
+ getElement().appendChild(anchor);
+ anchor.appendChild(captionElement);
+ addClickHandler(this);
+ setStyleName(CLASSNAME);
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (enabled) {
+ 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 (target == captionElement || target == anchor
+ || (icon != null && target == icon.getElement())) {
+ super.onBrowserEvent(event);
+ }
+ if (!enabled) {
+ event.preventDefault();
+ }
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java
new file mode 100644
index 0000000000..678847226b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/ListSelectConnector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.listselect;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector;
+import com.vaadin.ui.ListSelect;
+
+@Connect(ListSelect.class)
+public class ListSelectConnector extends OptionGroupBaseConnector {
+
+ @Override
+ public VListSelect getWidget() {
+ return (VListSelect) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java
new file mode 100644
index 0000000000..6f3f876fb7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/listselect/VListSelect.java
@@ -0,0 +1,112 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.listselect;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.user.client.ui.ListBox;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroupBase;
+
+public class VListSelect extends VOptionGroupBase {
+
+ public static final String CLASSNAME = "v-select";
+
+ private static final int VISIBLE_COUNT = 10;
+
+ protected ListBox select;
+
+ private int lastSelectedIndex = -1;
+
+ public VListSelect() {
+ super(new ListBox(true), CLASSNAME);
+ select = getOptionsContainer();
+ select.addChangeHandler(this);
+ select.addClickHandler(this);
+ select.setStyleName(CLASSNAME + "-select");
+ select.setVisibleItemCount(VISIBLE_COUNT);
+ }
+
+ protected ListBox getOptionsContainer() {
+ return (ListBox) optionsContainer;
+ }
+
+ @Override
+ protected void buildOptions(UIDL uidl) {
+ select.setMultipleSelect(isMultiselect());
+ select.setEnabled(!isDisabled() && !isReadonly());
+ select.clear();
+ if (!isMultiselect() && isNullSelectionAllowed()
+ && !isNullSelectionItemAvailable()) {
+ // can't unselect last item in singleselect mode
+ select.addItem("", (String) 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")) {
+ int itemIndex = select.getItemCount() - 1;
+ select.setItemSelected(itemIndex, true);
+ lastSelectedIndex = itemIndex;
+ }
+ }
+ if (getRows() > 0) {
+ select.setVisibleItemCount(getRows());
+ }
+ }
+
+ @Override
+ protected String[] getSelectedItems() {
+ final ArrayList<String> selectedItemKeys = new ArrayList<String>();
+ for (int i = 0; i < select.getItemCount(); i++) {
+ if (select.isItemSelected(i)) {
+ selectedItemKeys.add(select.getValue(i));
+ }
+ }
+ return selectedItemKeys.toArray(new String[selectedItemKeys.size()]);
+ }
+
+ @Override
+ public void onChange(ChangeEvent event) {
+ final int si = select.getSelectedIndex();
+ if (si == -1 && !isNullSelectionAllowed()) {
+ select.setSelectedIndex(lastSelectedIndex);
+ } else {
+ lastSelectedIndex = si;
+ if (isMultiselect()) {
+ client.updateVariable(paintableId, "selected",
+ getSelectedItems(), isImmediate());
+ } else {
+ client.updateVariable(paintableId, "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) {
+ getOptionsContainer().setTabIndex(tabIndex);
+ }
+
+ @Override
+ public void focus() {
+ select.setFocus(true);
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java
new file mode 100644
index 0000000000..5cb3fb83b4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBar.java
@@ -0,0 +1,520 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.menubar;
+
+/*
+ * 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.core.client.Scheduler;
+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.ui.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+/**
+ * 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<MenuItem> items = new ArrayList<MenuItem>();
+ 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;
+ }
+ }
+ }
+
+ @Override
+ 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
+ */
+ public List<MenuItem> 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
+ */
+ public 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.
+ */
+ protected 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) {
+ Scheduler.get().scheduleDeferred(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 VOverlay(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);
+ }
+ }
+ }
+
+ public 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 = 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(items.get(0));
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java
new file mode 100644
index 0000000000..2a8923bbc0
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuBarConnector.java
@@ -0,0 +1,191 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.menubar;
+
+import java.util.Iterator;
+import java.util.Stack;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.Command;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.menubar.VMenuBar.CustomMenuItem;
+
+@Connect(value = com.vaadin.ui.MenuBar.class, loadStyle = LoadStyle.LAZY)
+public class MenuBarConnector extends AbstractComponentConnector implements
+ Paintable, SimpleManagedLayout {
+ /**
+ * 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.
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().htmlContentAllowed = uidl
+ .hasAttribute(VMenuBar.HTML_CONTENT_ALLOWED);
+
+ getWidget().openRootOnHover = uidl
+ .getBooleanAttribute(VMenuBar.OPEN_ROOT_MENU_ON_HOWER);
+
+ getWidget().enabled = isEnabled();
+
+ // For future connections
+ getWidget().client = client;
+ getWidget().uidlId = uidl.getId();
+
+ // Empty the menu every time it receives new information
+ if (!getWidget().getItems().isEmpty()) {
+ getWidget().clearItems();
+ }
+
+ UIDL options = uidl.getChildUIDL(0);
+
+ if (null != getState() && !getState().isUndefinedWidth()) {
+ UIDL moreItemUIDL = options.getChildUIDL(0);
+ StringBuffer itemHTML = new StringBuffer();
+
+ if (moreItemUIDL.hasAttribute("icon")) {
+ itemHTML.append("<img src=\""
+ + Util.escapeAttribute(client
+ .translateVaadinUri(moreItemUIDL
+ .getStringAttribute("icon")))
+ + "\" class=\"" + Icon.CLASSNAME + "\" alt=\"\" />");
+ }
+
+ String moreItemText = moreItemUIDL.getStringAttribute("text");
+ if ("".equals(moreItemText)) {
+ moreItemText = "&#x25BA;";
+ }
+ itemHTML.append(moreItemText);
+
+ getWidget().moreItem = GWT.create(CustomMenuItem.class);
+ getWidget().moreItem.setHTML(itemHTML.toString());
+ getWidget().moreItem.setCommand(VMenuBar.emptyCommand);
+
+ getWidget().collapsedRootItems = new VMenuBar(true, getWidget());
+ getWidget().moreItem.setSubMenu(getWidget().collapsedRootItems);
+ getWidget().moreItem.addStyleName(VMenuBar.CLASSNAME
+ + "-more-menuitem");
+ }
+
+ UIDL uidlItems = uidl.getChildUIDL(1);
+ Iterator<Object> itr = uidlItems.getChildIterator();
+ Stack<Iterator<Object>> iteratorStack = new Stack<Iterator<Object>>();
+ Stack<VMenuBar> menuStack = new Stack<VMenuBar>();
+ VMenuBar currentMenu = getWidget();
+
+ while (itr.hasNext()) {
+ UIDL item = (UIDL) itr.next();
+ CustomMenuItem currentItem = null;
+
+ final int itemId = item.getIntAttribute("id");
+
+ boolean itemHasCommand = item.hasAttribute("command");
+ boolean itemIsCheckable = item
+ .hasAttribute(VMenuBar.ATTRIBUTE_CHECKED);
+
+ String itemHTML = getWidget().buildItemHTML(item);
+
+ Command cmd = null;
+ if (!item.hasAttribute("separator")) {
+ if (itemHasCommand || itemIsCheckable) {
+ // Construct a command that fires onMenuClick(int) with the
+ // item's id-number
+ cmd = new Command() {
+ @Override
+ public void execute() {
+ getWidget().hostReference.onMenuClick(itemId);
+ }
+ };
+ }
+ }
+
+ currentItem = currentMenu.addItem(itemHTML.toString(), cmd);
+ currentItem.updateFromUIDL(item, client);
+
+ if (item.getChildCount() > 0) {
+ menuStack.push(currentMenu);
+ iteratorStack.push(itr);
+ itr = item.getChildIterator();
+ currentMenu = new VMenuBar(true, currentMenu);
+ client.getVTooltip().connectHandlersToWidget(currentMenu);
+ // this is the top-level style that also propagates to items -
+ // any item specific styles are set above in
+ // currentItem.updateFromUIDL(item, client)
+ if (getState().hasStyles()) {
+ for (String style : getState().getStyles()) {
+ currentMenu.addStyleDependentName(style);
+ }
+ }
+ currentItem.setSubMenu(currentMenu);
+ }
+
+ while (!itr.hasNext() && !iteratorStack.empty()) {
+ boolean hasCheckableItem = false;
+ for (CustomMenuItem menuItem : currentMenu.getItems()) {
+ hasCheckableItem = hasCheckableItem
+ || menuItem.isCheckable();
+ }
+ if (hasCheckableItem) {
+ currentMenu.addStyleDependentName("check-column");
+ } else {
+ currentMenu.removeStyleDependentName("check-column");
+ }
+
+ itr = iteratorStack.pop();
+ currentMenu = menuStack.pop();
+ }
+ }// while
+
+ getLayoutManager().setNeedsHorizontalLayout(this);
+
+ }// updateFromUIDL
+
+ @Override
+ public VMenuBar getWidget() {
+ return (VMenuBar) super.getWidget();
+ }
+
+ @Override
+ public void layout() {
+ getWidget().iLayout();
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+ TooltipInfo info = null;
+
+ // Check content of widget to find tooltip for element
+ if (element != getWidget().getElement()) {
+
+ CustomMenuItem item = getWidget().getMenuItemWithElement(
+ (com.google.gwt.user.client.Element) element);
+ if (item != null) {
+ info = item.getTooltip();
+ }
+ }
+
+ // Use default tooltip if nothing found from DOM three
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java
new file mode 100644
index 0000000000..7f150d9a5f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/MenuItem.java
@@ -0,0 +1,193 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.menubar;
+
+/*
+ * 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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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/client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java
new file mode 100644
index 0000000000..47bda81362
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/menubar/VMenuBar.java
@@ -0,0 +1,1457 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.menubar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.Timer;
+import com.google.gwt.user.client.ui.HasHTML;
+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.BrowserInfo;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.SimpleFocusablePanel;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+public class VMenuBar extends SimpleFocusablePanel implements
+ CloseHandler<PopupPanel>, KeyPressHandler, KeyDownHandler,
+ FocusHandler, SubPartAware {
+
+ // The hierarchy of VMenuBar is a bit weird as VMenuBar is the Paintable,
+ // used for the root menu but also used for the sub menus.
+
+ /** Set the CSS class name to allow styling. */
+ public static final String CLASSNAME = "v-menubar";
+
+ /** For server connections **/
+ protected String uidlId;
+ protected ApplicationConnection client;
+
+ protected final VMenuBar hostReference = this;
+ protected CustomMenuItem moreItem = null;
+
+ // Only used by the root menu bar
+ protected VMenuBar collapsedRootItems;
+
+ // Construct an empty command to be used when the item has no command
+ // associated
+ protected static final Command emptyCommand = null;
+
+ public static final String OPEN_ROOT_MENU_ON_HOWER = "ormoh";
+
+ public static final String ATTRIBUTE_CHECKED = "checked";
+ public static final String ATTRIBUTE_ITEM_DESCRIPTION = "description";
+ public static final String ATTRIBUTE_ITEM_ICON = "icon";
+ public static final String ATTRIBUTE_ITEM_DISABLED = "disabled";
+ public static final String ATTRIBUTE_ITEM_STYLE = "style";
+
+ public static final String HTML_CONTENT_ALLOWED = "usehtml";
+
+ /** Widget fields **/
+ protected boolean subMenu;
+ protected ArrayList<CustomMenuItem> items;
+ protected Element containerElement;
+ protected VOverlay popup;
+ protected VMenuBar visibleChildMenu;
+ protected boolean menuVisible = false;
+ protected VMenuBar parentMenu;
+ protected CustomMenuItem selected;
+
+ boolean enabled = true;
+
+ private VLazyExecutor iconLoadedExecutioner = new VLazyExecutor(100,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ iLayout(true);
+ }
+ });
+
+ boolean openRootOnHover;
+
+ boolean htmlContentAllowed;
+
+ public VMenuBar() {
+ // Create an empty horizontal menubar
+ this(false, null);
+
+ // Navigation is only handled by the root bar
+ addFocusHandler(this);
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ addKeyPressHandler(this);
+ } else {
+ addKeyDownHandler(this);
+ }
+ }
+
+ public VMenuBar(boolean subMenu, VMenuBar parentMenu) {
+
+ items = new ArrayList<CustomMenuItem>();
+ popup = null;
+ visibleChildMenu = null;
+
+ containerElement = getElement();
+
+ if (!subMenu) {
+ setStyleName(CLASSNAME);
+ } else {
+ setStyleName(CLASSNAME + "-submenu");
+ this.parentMenu = parentMenu;
+ }
+ this.subMenu = subMenu;
+
+ sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT
+ | Event.ONLOAD);
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (!subMenu) {
+ setSelected(null);
+ hideChildren();
+ menuVisible = false;
+ }
+ }
+
+ void updateSize() {
+ // Take from setWidth
+ if (!subMenu) {
+ // Only needed for root level menu
+ hideChildren();
+ setSelected(null);
+ menuVisible = false;
+ }
+ }
+
+ /**
+ * Build the HTML content for a menu item.
+ *
+ * @param item
+ * @return
+ */
+ protected String buildItemHTML(UIDL item) {
+ // Construct html from the text and the optional icon
+ StringBuffer itemHTML = new StringBuffer();
+ if (item.hasAttribute("separator")) {
+ itemHTML.append("<span>---</span>");
+ } else {
+ // Add submenu indicator
+ if (item.getChildCount() > 0) {
+ String bgStyle = "";
+ itemHTML.append("<span class=\"" + CLASSNAME
+ + "-submenu-indicator\"" + bgStyle + ">&#x25BA;</span>");
+ }
+
+ itemHTML.append("<span class=\"" + CLASSNAME
+ + "-menuitem-caption\">");
+ if (item.hasAttribute("icon")) {
+ itemHTML.append("<img src=\""
+ + Util.escapeAttribute(client.translateVaadinUri(item
+ .getStringAttribute("icon"))) + "\" class=\""
+ + Icon.CLASSNAME + "\" alt=\"\" />");
+ }
+ String itemText = item.getStringAttribute("text");
+ if (!htmlContentAllowed) {
+ itemText = Util.escapeHTML(itemText);
+ }
+ itemHTML.append(itemText);
+ itemHTML.append("</span>");
+ }
+ return itemHTML.toString();
+ }
+
+ /**
+ * 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 = getContainerElement();
+ while (DOM.getChildCount(e) > 0) {
+ DOM.removeChild(e, DOM.getChild(e, 0));
+ }
+ items.clear();
+ }
+
+ /**
+ * Returns the containing element of the menu
+ *
+ * @return
+ */
+ @Override
+ public Element getContainerElement() {
+ return containerElement;
+ }
+
+ /**
+ * 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 = GWT.create(CustomMenuItem.class);
+ item.setHTML(html);
+ item.setCommand(cmd);
+
+ addItem(item);
+ return item;
+ }
+
+ /**
+ * Add a new item to this menu
+ *
+ * @param item
+ */
+ public void addItem(CustomMenuItem item) {
+ if (items.contains(item)) {
+ return;
+ }
+ DOM.appendChild(getContainerElement(), item.getElement());
+ item.setParentMenu(this);
+ item.setSelected(false);
+ items.add(item);
+ }
+
+ public void addItem(CustomMenuItem item, int index) {
+ if (items.contains(item)) {
+ return;
+ }
+ DOM.insertChild(getContainerElement(), item.getElement(), index);
+ item.setParentMenu(this);
+ item.setSelected(false);
+ items.add(index, item);
+ }
+
+ /**
+ * Remove the given item from this menu
+ *
+ * @param item
+ */
+ public void removeItem(CustomMenuItem item) {
+ if (items.contains(item)) {
+ int index = items.indexOf(item);
+
+ DOM.removeChild(getContainerElement(),
+ DOM.getChild(getContainerElement(), 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);
+
+ // Handle onload events (icon loaded, size changes)
+ if (DOM.eventGetType(e) == Event.ONLOAD) {
+ VMenuBar parent = getParentMenu();
+ if (parent != null) {
+ // The onload event for an image in a popup should be sent to
+ // the parent, which owns the popup
+ parent.iconLoaded();
+ } else {
+ // Onload events for images in the root menu are handled by the
+ // root menu itself
+ iconLoaded();
+ }
+ return;
+ }
+
+ Element targetElement = DOM.eventGetTarget(e);
+ CustomMenuItem targetItem = null;
+ for (int i = 0; i < items.size(); i++) {
+ CustomMenuItem item = items.get(i);
+ if (DOM.isOrHasChild(item.getElement(), targetElement)) {
+ targetItem = item;
+ }
+ }
+
+ if (targetItem != null) {
+ switch (DOM.eventGetType(e)) {
+
+ case Event.ONCLICK:
+ if (isEnabled() && targetItem.isEnabled()) {
+ itemClick(targetItem);
+ }
+ if (subMenu) {
+ // Prevent moving keyboard focus to child menus
+ VMenuBar parent = parentMenu;
+ while (parent.getParentMenu() != null) {
+ parent = parent.getParentMenu();
+ }
+ parent.setFocus(true);
+ }
+
+ break;
+
+ case Event.ONMOUSEOVER:
+ LazyCloser.cancelClosing();
+
+ if (isEnabled() && targetItem.isEnabled()) {
+ itemOver(targetItem);
+ }
+ break;
+
+ case Event.ONMOUSEOUT:
+ itemOut(targetItem);
+ LazyCloser.schedule();
+ break;
+ }
+ } else if (subMenu && DOM.eventGetType(e) == Event.ONCLICK && subMenu) {
+ // Prevent moving keyboard focus to child menus
+ VMenuBar parent = parentMenu;
+ while (parent.getParentMenu() != null) {
+ parent = parent.getParentMenu();
+ }
+ parent.setFocus(true);
+ }
+ }
+
+ private boolean isEnabled() {
+ return enabled;
+ }
+
+ private void iconLoaded() {
+ iconLoadedExecutioner.trigger();
+ }
+
+ /**
+ * When an item is clicked
+ *
+ * @param item
+ */
+ public void itemClick(CustomMenuItem item) {
+ if (item.getCommand() != null) {
+ setSelected(null);
+
+ if (visibleChildMenu != null) {
+ visibleChildMenu.hideChildren();
+ }
+
+ hideParents(true);
+ menuVisible = false;
+ Scheduler.get().scheduleDeferred(item.getCommand());
+
+ } else {
+ if (item.getSubMenu() != null
+ && item.getSubMenu() != visibleChildMenu) {
+ setSelected(item);
+ showChildMenu(item);
+ menuVisible = true;
+ } else if (!subMenu) {
+ setSelected(null);
+ hideChildren();
+ menuVisible = false;
+ }
+ }
+ }
+
+ /**
+ * When the user hovers the mouse over the item
+ *
+ * @param item
+ */
+ public void itemOver(CustomMenuItem item) {
+ if ((openRootOnHover || subMenu || menuVisible) && !item.isSeparator()) {
+ setSelected(item);
+ if (!subMenu && openRootOnHover && !menuVisible) {
+ menuVisible = true; // start opening menus
+ LazyCloser.prepare(this);
+ }
+ }
+
+ if (menuVisible && visibleChildMenu != item.getSubMenu()
+ && popup != null) {
+ popup.hide();
+ }
+
+ if (menuVisible && item.getSubMenu() != null
+ && 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()) {
+ hideChildMenu(item);
+ setSelected(null);
+ } else if (visibleChildMenu == null) {
+ setSelected(null);
+ }
+ }
+
+ /**
+ * Used to autoclose submenus when they the menu is in a mode which opens
+ * root menus on mouse hover.
+ */
+ private static class LazyCloser extends Timer {
+ static LazyCloser INSTANCE;
+ private VMenuBar activeRoot;
+
+ @Override
+ public void run() {
+ activeRoot.hideChildren();
+ activeRoot.setSelected(null);
+ activeRoot.menuVisible = false;
+ activeRoot = null;
+ }
+
+ public static void cancelClosing() {
+ if (INSTANCE != null) {
+ INSTANCE.cancel();
+ }
+ }
+
+ public static void prepare(VMenuBar vMenuBar) {
+ if (INSTANCE == null) {
+ INSTANCE = new LazyCloser();
+ }
+ if (INSTANCE.activeRoot == vMenuBar) {
+ INSTANCE.cancel();
+ } else if (INSTANCE.activeRoot != null) {
+ INSTANCE.cancel();
+ INSTANCE.run();
+ }
+ INSTANCE.activeRoot = vMenuBar;
+ }
+
+ public static void schedule() {
+ if (INSTANCE != null && INSTANCE.activeRoot != null) {
+ INSTANCE.schedule(750);
+ }
+ }
+
+ }
+
+ /**
+ * Shows the child menu of an item. The caller must ensure that the item has
+ * a submenu.
+ *
+ * @param item
+ */
+ public void showChildMenu(CustomMenuItem item) {
+
+ int left = 0;
+ int top = 0;
+ if (subMenu) {
+ left = item.getParentMenu().getAbsoluteLeft()
+ + item.getParentMenu().getOffsetWidth();
+ top = item.getAbsoluteTop();
+ } else {
+ left = item.getAbsoluteLeft();
+ top = item.getParentMenu().getAbsoluteTop()
+ + item.getParentMenu().getOffsetHeight();
+ }
+ showChildMenuAt(item, top, left);
+ }
+
+ protected void showChildMenuAt(CustomMenuItem item, int top, int left) {
+ final int shadowSpace = 10;
+
+ popup = new VOverlay(true, false, true);
+
+ // Setting owner and handlers to support tooltips. Needed for tooltip
+ // handling of overlay widgets (will direct queries to parent menu)
+ if (parentMenu == null) {
+ popup.setOwner(this);
+ } else {
+ VMenuBar parent = parentMenu;
+ while (parent.getParentMenu() != null) {
+ parent = parent.getParentMenu();
+ }
+ popup.setOwner(parent);
+ }
+ if (client != null) {
+ client.getVTooltip().connectHandlersToWidget(popup);
+ }
+
+ popup.setStyleName(CLASSNAME + "-popup");
+ popup.setWidget(item.getSubMenu());
+ popup.addCloseHandler(this);
+ popup.addAutoHidePartner(item.getElement());
+
+ // at 0,0 because otherwise IE7 add extra scrollbars (#5547)
+ popup.setPopupPosition(0, 0);
+
+ item.getSubMenu().onShow();
+ visibleChildMenu = item.getSubMenu();
+ item.getSubMenu().setParentMenu(this);
+
+ popup.show();
+
+ if (left + popup.getOffsetWidth() >= RootPanel.getBodyElement()
+ .getOffsetWidth() - shadowSpace) {
+ if (subMenu) {
+ left = item.getParentMenu().getAbsoluteLeft()
+ - popup.getOffsetWidth() - shadowSpace;
+ } else {
+ left = RootPanel.getBodyElement().getOffsetWidth()
+ - popup.getOffsetWidth() - shadowSpace;
+ }
+ // Accommodate space for shadow
+ if (left < shadowSpace) {
+ left = shadowSpace;
+ }
+ }
+
+ top = adjustPopupHeight(top, shadowSpace);
+
+ popup.setPopupPosition(left, top);
+
+ }
+
+ private int adjustPopupHeight(int top, final int shadowSpace) {
+ // Check that the popup will fit the screen
+ int availableHeight = RootPanel.getBodyElement().getOffsetHeight()
+ - top - shadowSpace;
+ int missingHeight = popup.getOffsetHeight() - availableHeight;
+ if (missingHeight > 0) {
+ // First move the top of the popup to get more space
+ // Don't move above top of screen, don't move more than needed
+ int moveUpBy = Math.min(top - shadowSpace, missingHeight);
+
+ // Update state
+ top -= moveUpBy;
+ missingHeight -= moveUpBy;
+ availableHeight += moveUpBy;
+
+ if (missingHeight > 0) {
+ int contentWidth = visibleChildMenu.getOffsetWidth();
+
+ // If there's still not enough room, limit height to fit and add
+ // a scroll bar
+ Style style = popup.getElement().getStyle();
+ style.setHeight(availableHeight, Unit.PX);
+ style.setOverflowY(Overflow.SCROLL);
+
+ // Make room for the scroll bar by adjusting the width of the
+ // popup
+ style.setWidth(contentWidth + Util.getNativeScrollbarSize(),
+ Unit.PX);
+ popup.sizeOrPositionUpdated();
+ }
+ }
+ return top;
+ }
+
+ /**
+ * 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() {
+ // remove possible previous selection
+ if (selected != null) {
+ selected.setSelected(false);
+ selected = null;
+ }
+ menuVisible = true;
+ }
+
+ /**
+ * Listener method, fired when this menu is closed
+ */
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ hideChildren();
+ if (event.isAutoClosed()) {
+ hideParents(true);
+ menuVisible = false;
+ }
+ visibleChildMenu = null;
+ popup = null;
+ }
+
+ /**
+ * Recursively hide all child menus
+ */
+ public void hideChildren() {
+ if (visibleChildMenu != null) {
+ visibleChildMenu.hideChildren();
+ popup.hide();
+ }
+ }
+
+ /**
+ * Recursively hide all parent menus
+ */
+ public void hideParents(boolean autoClosed) {
+ if (visibleChildMenu != null) {
+ popup.hide();
+ setSelected(null);
+ menuVisible = !autoClosed;
+ }
+
+ if (getParentMenu() != null) {
+ getParentMenu().hideParents(autoClosed);
+ }
+ }
+
+ /**
+ * Returns the parent menu of this menu, or null if this is the top-level
+ * menu
+ *
+ * @return
+ */
+ public VMenuBar getParentMenu() {
+ return parentMenu;
+ }
+
+ /**
+ * Set the parent menu of this menu
+ *
+ * @param parent
+ */
+ public void setParentMenu(VMenuBar 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;
+ }
+
+ /**
+ *
+ * A class to hold information on menu items
+ *
+ */
+ public static class CustomMenuItem extends Widget implements HasHTML {
+
+ protected String html = null;
+ protected Command command = null;
+ protected VMenuBar subMenu = null;
+ protected VMenuBar parentMenu = null;
+ protected boolean enabled = true;
+ protected boolean isSeparator = false;
+ protected boolean checkable = false;
+ protected boolean checked = false;
+ protected String description = null;
+
+ /**
+ * Default menu item {@link Widget} constructor for GWT.create().
+ *
+ * Use {@link #setHTML(String)} and {@link #setCommand(Command)} after
+ * constructing a menu item.
+ */
+ public CustomMenuItem() {
+ this("", null);
+ }
+
+ /**
+ * Creates a menu item {@link Widget}.
+ *
+ * @param html
+ * @param cmd
+ * @deprecated use the default constructor and {@link #setHTML(String)}
+ * and {@link #setCommand(Command)} instead
+ */
+ @Deprecated
+ public CustomMenuItem(String html, Command cmd) {
+ // We need spans to allow inline-block in IE
+ setElement(DOM.createSpan());
+
+ setHTML(html);
+ setCommand(cmd);
+ setSelected(false);
+ setStyleName(CLASSNAME + "-menuitem");
+
+ }
+
+ public void setSelected(boolean selected) {
+ if (selected && isSelectable()) {
+ addStyleDependentName("selected");
+ // needed for IE6 to have a single style name to match for an
+ // element
+ // TODO Can be optimized now that IE6 is not supported any more
+ if (checkable) {
+ if (checked) {
+ removeStyleDependentName("selected-unchecked");
+ addStyleDependentName("selected-checked");
+ } else {
+ removeStyleDependentName("selected-checked");
+ addStyleDependentName("selected-unchecked");
+ }
+ }
+ } else {
+ removeStyleDependentName("selected");
+ // needed for IE6 to have a single style name to match for an
+ // element
+ removeStyleDependentName("selected-checked");
+ removeStyleDependentName("selected-unchecked");
+ }
+ }
+
+ public void setChecked(boolean checked) {
+ if (checkable && !isSeparator) {
+ this.checked = checked;
+
+ if (checked) {
+ addStyleDependentName("checked");
+ removeStyleDependentName("unchecked");
+ } else {
+ addStyleDependentName("unchecked");
+ removeStyleDependentName("checked");
+ }
+ } else {
+ this.checked = false;
+ }
+ }
+
+ public boolean isChecked() {
+ return checked;
+ }
+
+ public void setCheckable(boolean checkable) {
+ if (checkable && !isSeparator) {
+ this.checkable = true;
+ } else {
+ setChecked(false);
+ this.checkable = false;
+ }
+ }
+
+ public boolean isCheckable() {
+ return checkable;
+ }
+
+ /*
+ * setters and getters for the fields
+ */
+
+ public void setSubMenu(VMenuBar subMenu) {
+ this.subMenu = subMenu;
+ }
+
+ public VMenuBar getSubMenu() {
+ return subMenu;
+ }
+
+ public void setParentMenu(VMenuBar parentMenu) {
+ this.parentMenu = parentMenu;
+ }
+
+ public VMenuBar getParentMenu() {
+ return parentMenu;
+ }
+
+ public void setCommand(Command command) {
+ this.command = command;
+ }
+
+ public Command getCommand() {
+ return command;
+ }
+
+ @Override
+ public String getHTML() {
+ return html;
+ }
+
+ @Override
+ public void setHTML(String html) {
+ this.html = html;
+ DOM.setInnerHTML(getElement(), html);
+
+ // Sink the onload event for any icons. The onload
+ // events are handled by the parent VMenuBar.
+ Util.sinkOnloadForImages(getElement());
+ }
+
+ @Override
+ public String getText() {
+ return html;
+ }
+
+ @Override
+ public void setText(String text) {
+ setHTML(Util.escapeHTML(text));
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ if (enabled) {
+ removeStyleDependentName("disabled");
+ } else {
+ addStyleDependentName("disabled");
+ }
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ private void setSeparator(boolean separator) {
+ isSeparator = separator;
+ if (separator) {
+ setStyleName(CLASSNAME + "-separator");
+ } else {
+ setStyleName(CLASSNAME + "-menuitem");
+ setEnabled(enabled);
+ }
+ }
+
+ public boolean isSeparator() {
+ return isSeparator;
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ setSeparator(uidl.hasAttribute("separator"));
+ setEnabled(!uidl.hasAttribute(ATTRIBUTE_ITEM_DISABLED));
+
+ if (!isSeparator() && uidl.hasAttribute(ATTRIBUTE_CHECKED)) {
+ // if the selected attribute is present (either true or false),
+ // the item is selectable
+ setCheckable(true);
+ setChecked(uidl.getBooleanAttribute(ATTRIBUTE_CHECKED));
+ } else {
+ setCheckable(false);
+ }
+
+ if (uidl.hasAttribute(ATTRIBUTE_ITEM_STYLE)) {
+ String itemStyle = uidl
+ .getStringAttribute(ATTRIBUTE_ITEM_STYLE);
+ addStyleDependentName(itemStyle);
+ }
+
+ if (uidl.hasAttribute(ATTRIBUTE_ITEM_DESCRIPTION)) {
+ description = uidl
+ .getStringAttribute(ATTRIBUTE_ITEM_DESCRIPTION);
+ }
+ }
+
+ public TooltipInfo getTooltip() {
+ if (description == null) {
+ return null;
+ }
+
+ return new TooltipInfo(description);
+ }
+
+ /**
+ * Checks if the item can be selected.
+ *
+ * @return true if it is possible to select this item, false otherwise
+ */
+ public boolean isSelectable() {
+ return !isSeparator() && isEnabled();
+ }
+
+ }
+
+ /**
+ * @author Jouni Koivuviita / Vaadin Ltd.
+ */
+ public void iLayout() {
+ iLayout(false);
+ updateSize();
+ }
+
+ public void iLayout(boolean iconLoadEvent) {
+ // Only collapse if there is more than one item in the root menu and the
+ // menu has an explicit size
+ if ((getItems().size() > 1 || (collapsedRootItems != null && collapsedRootItems
+ .getItems().size() > 0))
+ && getElement().getStyle().getProperty("width") != null
+ && moreItem != null) {
+
+ // Measure the width of the "more" item
+ final boolean morePresent = getItems().contains(moreItem);
+ addItem(moreItem);
+ final int moreItemWidth = moreItem.getOffsetWidth();
+ if (!morePresent) {
+ removeItem(moreItem);
+ }
+
+ int availableWidth = LayoutManager.get(client).getInnerWidth(
+ getElement());
+
+ // Used width includes the "more" item if present
+ int usedWidth = getConsumedWidth();
+ int diff = availableWidth - usedWidth;
+ removeItem(moreItem);
+
+ if (diff < 0) {
+ // Too many items: collapse last items from root menu
+ int widthNeeded = usedWidth - availableWidth;
+ if (!morePresent) {
+ widthNeeded += moreItemWidth;
+ }
+ int widthReduced = 0;
+
+ while (widthReduced < widthNeeded && getItems().size() > 0) {
+ // Move last root menu item to collapsed menu
+ CustomMenuItem collapse = getItems().get(
+ getItems().size() - 1);
+ widthReduced += collapse.getOffsetWidth();
+ removeItem(collapse);
+ collapsedRootItems.addItem(collapse, 0);
+ }
+ } else if (collapsedRootItems.getItems().size() > 0) {
+ // Space available for items: expand first items from collapsed
+ // menu
+ int widthAvailable = diff + moreItemWidth;
+ int widthGrowth = 0;
+
+ while (widthAvailable > widthGrowth
+ && collapsedRootItems.getItems().size() > 0) {
+ // Move first item from collapsed menu to the root menu
+ CustomMenuItem expand = collapsedRootItems.getItems()
+ .get(0);
+ collapsedRootItems.removeItem(expand);
+ addItem(expand);
+ widthGrowth += expand.getOffsetWidth();
+ if (collapsedRootItems.getItems().size() > 0) {
+ widthAvailable -= moreItemWidth;
+ }
+ if (widthGrowth > widthAvailable) {
+ removeItem(expand);
+ collapsedRootItems.addItem(expand, 0);
+ } else {
+ widthAvailable = diff + moreItemWidth;
+ }
+ }
+ }
+ if (collapsedRootItems.getItems().size() > 0) {
+ addItem(moreItem);
+ }
+ }
+
+ // If a popup is open we might need to adjust the shadow as well if an
+ // icon shown in that popup was loaded
+ if (popup != null) {
+ // Forces a recalculation of the shadow size
+ popup.show();
+ }
+ if (iconLoadEvent) {
+ // Size have changed if the width is undefined
+ Util.notifyParentOfSizeChange(this, false);
+ }
+ }
+
+ private int getConsumedWidth() {
+ int w = 0;
+ for (CustomMenuItem item : getItems()) {
+ if (!collapsedRootItems.getItems().contains(item)) {
+ w += item.getOffsetWidth();
+ }
+ }
+ return w;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
+ * .gwt.event.dom.client.KeyPressEvent)
+ */
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ if (handleNavigation(event.getNativeEvent().getKeyCode(),
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (handleNavigation(event.getNativeEvent().getKeyCode(),
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Get the key that moves the selection upwards. By default it is the up
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection downwards. By default it is the down
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that moves the selection left. By default it is the left
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that moves the selection right. By default it is the right
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Get the key that selects a menu item. By default it is the Enter key but
+ * by overriding this you can change the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return KeyCodes.KEY_ENTER;
+ }
+
+ /**
+ * Get the key that closes the menu. By default it is the escape key but by
+ * overriding this yoy can change the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getCloseMenuKey() {
+ return KeyCodes.KEY_ESCAPE;
+ }
+
+ /**
+ * Handles the keyboard events handled by the MenuBar
+ *
+ * @param event
+ * The keyboard event received
+ * @return true iff the navigation event was handled
+ */
+ public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+
+ // If tab or shift+tab close menus
+ if (keycode == KeyCodes.KEY_TAB) {
+ setSelected(null);
+ hideChildren();
+ menuVisible = false;
+ return false;
+ }
+
+ if (ctrl || shift || !isEnabled()) {
+ // Do not handle tab key, nor ctrl keys
+ return false;
+ }
+
+ if (keycode == getNavigationLeftKey()) {
+ if (getSelected() == null) {
+ // If nothing is selected then select the last item
+ setSelected(items.get(items.size() - 1));
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ } else if (visibleChildMenu == null && getParentMenu() == null) {
+ // If this is the root menu then move to the left
+ int idx = items.indexOf(getSelected());
+ if (idx > 0) {
+ setSelected(items.get(idx - 1));
+ } else {
+ setSelected(items.get(items.size() - 1));
+ }
+
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ } else if (visibleChildMenu != null) {
+ // Redirect all navigation to the submenu
+ visibleChildMenu.handleNavigation(keycode, ctrl, shift);
+
+ } else if (getParentMenu().getParentMenu() == null) {
+ // Inside a sub menu, whose parent is a root menu item
+ VMenuBar root = getParentMenu();
+
+ root.getSelected().getSubMenu().setSelected(null);
+ root.hideChildren();
+
+ // Get the root menus items and select the previous one
+ int idx = root.getItems().indexOf(root.getSelected());
+ idx = idx > 0 ? idx : root.getItems().size();
+ CustomMenuItem selected = root.getItems().get(--idx);
+
+ while (selected.isSeparator() || !selected.isEnabled()) {
+ idx = idx > 0 ? idx : root.getItems().size();
+ selected = root.getItems().get(--idx);
+ }
+
+ root.setSelected(selected);
+ openMenuAndFocusFirstIfPossible(selected);
+ } else {
+ getParentMenu().getSelected().getSubMenu().setSelected(null);
+ getParentMenu().hideChildren();
+ }
+
+ return true;
+
+ } else if (keycode == getNavigationRightKey()) {
+
+ if (getSelected() == null) {
+ // If nothing is selected then select the first item
+ setSelected(items.get(0));
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ } else if (visibleChildMenu == null && getParentMenu() == null) {
+ // If this is the root menu then move to the right
+ int idx = items.indexOf(getSelected());
+
+ if (idx < items.size() - 1) {
+ setSelected(items.get(idx + 1));
+ } else {
+ setSelected(items.get(0));
+ }
+
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ } else if (visibleChildMenu == null
+ && getSelected().getSubMenu() != null) {
+ // If the item has a submenu then show it and move the selection
+ // there
+ showChildMenu(getSelected());
+ menuVisible = true;
+ visibleChildMenu.handleNavigation(keycode, ctrl, shift);
+ } else if (visibleChildMenu == null) {
+
+ // Get the root menu
+ VMenuBar root = getParentMenu();
+ while (root.getParentMenu() != null) {
+ root = root.getParentMenu();
+ }
+
+ // Hide the submenu
+ root.hideChildren();
+
+ // Get the root menus items and select the next one
+ int idx = root.getItems().indexOf(root.getSelected());
+ idx = idx < root.getItems().size() - 1 ? idx : -1;
+ CustomMenuItem selected = root.getItems().get(++idx);
+
+ while (selected.isSeparator() || !selected.isEnabled()) {
+ idx = idx < root.getItems().size() - 1 ? idx : -1;
+ selected = root.getItems().get(++idx);
+ }
+
+ root.setSelected(selected);
+ openMenuAndFocusFirstIfPossible(selected);
+ } else if (visibleChildMenu != null) {
+ // Redirect all navigation to the submenu
+ visibleChildMenu.handleNavigation(keycode, ctrl, shift);
+ }
+
+ return true;
+
+ } else if (keycode == getNavigationUpKey()) {
+
+ if (getSelected() == null) {
+ // If nothing is selected then select the last item
+ setSelected(items.get(items.size() - 1));
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ } else if (visibleChildMenu != null) {
+ // Redirect all navigation to the submenu
+ visibleChildMenu.handleNavigation(keycode, ctrl, shift);
+ } else {
+ // Select the previous item if possible or loop to the last item
+ int idx = items.indexOf(getSelected());
+ if (idx > 0) {
+ setSelected(items.get(idx - 1));
+ } else {
+ setSelected(items.get(items.size() - 1));
+ }
+
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ }
+
+ return true;
+
+ } else if (keycode == getNavigationDownKey()) {
+
+ if (getSelected() == null) {
+ // If nothing is selected then select the first item
+ selectFirstItem();
+ } else if (visibleChildMenu == null && getParentMenu() == null) {
+ // If this is the root menu the show the child menu with arrow
+ // down, if there is a child menu
+ openMenuAndFocusFirstIfPossible(getSelected());
+ } else if (visibleChildMenu != null) {
+ // Redirect all navigation to the submenu
+ visibleChildMenu.handleNavigation(keycode, ctrl, shift);
+ } else {
+ // Select the next item if possible or loop to the first item
+ int idx = items.indexOf(getSelected());
+ if (idx < items.size() - 1) {
+ setSelected(items.get(idx + 1));
+ } else {
+ setSelected(items.get(0));
+ }
+
+ if (!getSelected().isSelectable()) {
+ handleNavigation(keycode, ctrl, shift);
+ }
+ }
+ return true;
+
+ } else if (keycode == getCloseMenuKey()) {
+ setSelected(null);
+ hideChildren();
+ menuVisible = false;
+
+ } else if (keycode == getNavigationSelectKey()) {
+ if (getSelected() == null) {
+ // If nothing is selected then select the first item
+ selectFirstItem();
+ } else if (visibleChildMenu != null) {
+ // Redirect all navigation to the submenu
+ visibleChildMenu.handleNavigation(keycode, ctrl, shift);
+ menuVisible = false;
+ } else if (visibleChildMenu == null
+ && getSelected().getSubMenu() != null) {
+ // If the item has a sub menu then show it and move the
+ // selection there
+ openMenuAndFocusFirstIfPossible(getSelected());
+ } else {
+ Command command = getSelected().getCommand();
+ if (command != null) {
+ command.execute();
+ }
+
+ setSelected(null);
+ hideParents(true);
+ }
+ }
+
+ return false;
+ }
+
+ private void selectFirstItem() {
+ for (int i = 0; i < items.size(); i++) {
+ CustomMenuItem item = items.get(i);
+ if (item.isSelectable()) {
+ setSelected(item);
+ break;
+ }
+ }
+ }
+
+ private void openMenuAndFocusFirstIfPossible(CustomMenuItem menuItem) {
+ VMenuBar subMenu = menuItem.getSubMenu();
+ if (subMenu == null) {
+ // No child menu? Nothing to do
+ return;
+ }
+
+ VMenuBar parentMenu = menuItem.getParentMenu();
+ parentMenu.showChildMenu(menuItem);
+
+ menuVisible = true;
+ // Select the first item in the newly open submenu
+ subMenu.selectFirstItem();
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ @Override
+ public void onFocus(FocusEvent event) {
+
+ }
+
+ private final String SUBPART_PREFIX = "item";
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ int index = Integer
+ .parseInt(subPart.substring(SUBPART_PREFIX.length()));
+ CustomMenuItem item = getItems().get(index);
+
+ return item.getElement();
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ Element menuItemRoot = subElement;
+ while (menuItemRoot != null && menuItemRoot.getParentElement() != null
+ && menuItemRoot.getParentElement() != getElement()) {
+ menuItemRoot = menuItemRoot.getParentElement().cast();
+ }
+ // "menuItemRoot" is now the root of the menu item
+
+ final int itemCount = getItems().size();
+ for (int i = 0; i < itemCount; i++) {
+ if (getItems().get(i).getElement() == menuItemRoot) {
+ String name = SUBPART_PREFIX + i;
+ return name;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get menu item with given DOM element
+ *
+ * @param element
+ * Element used in search
+ * @return Menu item or null if not found
+ */
+ public CustomMenuItem getMenuItemWithElement(Element element) {
+ for (int i = 0; i < items.size(); i++) {
+ CustomMenuItem item = items.get(i);
+ if (DOM.isOrHasChild(item.getElement(), element)) {
+ return item;
+ }
+
+ if (item.getSubMenu() != null) {
+ item = item.getSubMenu().getMenuItemWithElement(element);
+ if (item != null) {
+ return item;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java
new file mode 100644
index 0000000000..73de87c276
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/NativeButtonConnector.java
@@ -0,0 +1,124 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.nativebutton;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.button.ButtonServerRpc;
+import com.vaadin.shared.ui.button.ButtonState;
+import com.vaadin.terminal.gwt.client.EventHelper;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.ui.NativeButton;
+
+@Connect(NativeButton.class)
+public class NativeButtonConnector extends AbstractComponentConnector implements
+ BlurHandler, FocusHandler {
+
+ private HandlerRegistration focusHandlerRegistration;
+ private HandlerRegistration blurHandlerRegistration;
+
+ private FocusAndBlurServerRpc focusBlurRpc = RpcProxy.create(
+ FocusAndBlurServerRpc.class, this);
+
+ @Override
+ public void init() {
+ super.init();
+
+ getWidget().buttonRpcProxy = RpcProxy.create(ButtonServerRpc.class,
+ this);
+ getWidget().client = getConnection();
+ getWidget().paintableId = getConnectorId();
+ }
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ getWidget().disableOnClick = getState().isDisableOnClick();
+ focusHandlerRegistration = EventHelper.updateFocusHandler(this,
+ focusHandlerRegistration);
+ blurHandlerRegistration = EventHelper.updateBlurHandler(this,
+ blurHandlerRegistration);
+
+ // Set text
+ if (getState().isHtmlContentAllowed()) {
+ getWidget().setHTML(getState().getCaption());
+ } else {
+ getWidget().setText(getState().getCaption());
+ }
+
+ // handle error
+ if (null != getState().getErrorMessage()) {
+ if (getWidget().errorIndicatorElement == null) {
+ getWidget().errorIndicatorElement = DOM.createSpan();
+ getWidget().errorIndicatorElement
+ .setClassName("v-errorindicator");
+ }
+ getWidget().getElement().insertBefore(
+ getWidget().errorIndicatorElement,
+ getWidget().captionElement);
+
+ } else if (getWidget().errorIndicatorElement != null) {
+ getWidget().getElement().removeChild(
+ getWidget().errorIndicatorElement);
+ getWidget().errorIndicatorElement = null;
+ }
+
+ if (getState().getIcon() != null) {
+ if (getWidget().icon == null) {
+ getWidget().icon = new Icon(getConnection());
+ getWidget().getElement().insertBefore(
+ getWidget().icon.getElement(),
+ getWidget().captionElement);
+ }
+ getWidget().icon.setUri(getState().getIcon().getURL());
+ } else {
+ if (getWidget().icon != null) {
+ getWidget().getElement().removeChild(
+ getWidget().icon.getElement());
+ getWidget().icon = null;
+ }
+ }
+
+ }
+
+ @Override
+ public VNativeButton getWidget() {
+ return (VNativeButton) super.getWidget();
+ }
+
+ @Override
+ public ButtonState getState() {
+ return (ButtonState) super.getState();
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ // EventHelper.updateFocusHandler ensures that this is called only when
+ // there is a listener on server side
+ focusBlurRpc.focus();
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ // EventHelper.updateFocusHandler ensures that this is called only when
+ // there is a listener on server side
+ focusBlurRpc.blur();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java
new file mode 100644
index 0000000000..d58fd2a995
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativebutton/VNativeButton.java
@@ -0,0 +1,125 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.nativebutton;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Button;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.button.ButtonServerRpc;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+
+public class VNativeButton extends Button implements ClickHandler {
+
+ public static final String CLASSNAME = "v-nativebutton";
+
+ protected String width = null;
+
+ protected String paintableId;
+
+ protected ApplicationConnection client;
+
+ ButtonServerRpc buttonRpcProxy;
+
+ protected Element errorIndicatorElement;
+
+ protected final Element captionElement = DOM.createSpan();
+
+ protected Icon icon;
+
+ /**
+ * Helper flag 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;
+
+ protected boolean disableOnClick = false;
+
+ public VNativeButton() {
+ setStyleName(CLASSNAME);
+
+ getElement().appendChild(captionElement);
+ captionElement.setClassName(getStyleName() + "-caption");
+
+ addClickHandler(this);
+
+ sinkEvents(Event.ONMOUSEDOWN);
+ sinkEvents(Event.ONMOUSEUP);
+ }
+
+ @Override
+ public void setText(String text) {
+ captionElement.setInnerText(text);
+ }
+
+ @Override
+ public void setHTML(String html) {
+ captionElement.setInnerHTML(html);
+ }
+
+ @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;
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEMOVE) {
+ clickPending = false;
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEOUT) {
+ if (clickPending) {
+ click();
+ }
+ clickPending = false;
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ this.width = width;
+ super.setWidth(width);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event
+ * .dom.client.ClickEvent)
+ */
+ @Override
+ public void onClick(ClickEvent event) {
+ if (paintableId == null || client == null) {
+ return;
+ }
+
+ if (BrowserInfo.get().isSafari()) {
+ VNativeButton.this.setFocus(true);
+ }
+ if (disableOnClick) {
+ setEnabled(false);
+ buttonRpcProxy.disableOnClick();
+ }
+
+ // Add mouse details
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event.getNativeEvent(), getElement());
+ buttonRpcProxy.click(details);
+
+ clickPending = false;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java
new file mode 100644
index 0000000000..8a00afa056
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/NativeSelectConnector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.nativeselect;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector;
+import com.vaadin.ui.NativeSelect;
+
+@Connect(NativeSelect.class)
+public class NativeSelectConnector extends OptionGroupBaseConnector {
+
+ @Override
+ public VNativeSelect getWidget() {
+ return (VNativeSelect) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java
new file mode 100644
index 0000000000..ea5292724d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/nativeselect/VNativeSelect.java
@@ -0,0 +1,115 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.nativeselect;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.user.client.ui.ListBox;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroupBase;
+
+public class VNativeSelect extends VOptionGroupBase implements Field {
+
+ public static final String CLASSNAME = "v-select";
+
+ protected ListBox select;
+
+ private boolean firstValueIsTemporaryNullItem = false;
+
+ public VNativeSelect() {
+ super(new ListBox(false), CLASSNAME);
+ select = getOptionsContainer();
+ select.setVisibleItemCount(1);
+ select.addChangeHandler(this);
+ select.setStyleName(CLASSNAME + "-select");
+
+ }
+
+ protected ListBox getOptionsContainer() {
+ return (ListBox) optionsContainer;
+ }
+
+ @Override
+ protected void buildOptions(UIDL uidl) {
+ select.setEnabled(!isDisabled() && !isReadonly());
+ select.clear();
+ firstValueIsTemporaryNullItem = false;
+
+ if (isNullSelectionAllowed() && !isNullSelectionItemAvailable()) {
+ // can't unselect last item in singleselect mode
+ select.addItem("", (String) 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("", (String) null, 0);
+ select.setItemSelected(0, true);
+ firstValueIsTemporaryNullItem = true;
+ }
+ }
+
+ @Override
+ protected String[] getSelectedItems() {
+ final ArrayList<String> selectedItemKeys = new ArrayList<String>();
+ for (int i = 0; i < select.getItemCount(); i++) {
+ if (select.isItemSelected(i)) {
+ selectedItemKeys.add(select.getValue(i));
+ }
+ }
+ return selectedItemKeys.toArray(new String[selectedItemKeys.size()]);
+ }
+
+ @Override
+ public void onChange(ChangeEvent event) {
+
+ if (select.isMultipleSelect()) {
+ client.updateVariable(paintableId, "selected", getSelectedItems(),
+ isImmediate());
+ } else {
+ client.updateVariable(paintableId, "selected", new String[] { ""
+ + getSelectedItem() }, isImmediate());
+ }
+ if (firstValueIsTemporaryNullItem) {
+ // remove temporary empty item
+ select.removeItem(0);
+ firstValueIsTemporaryNullItem = false;
+ }
+ }
+
+ @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) {
+ getOptionsContainer().setTabIndex(tabIndex);
+ }
+
+ @Override
+ public void focus() {
+ select.setFocus(true);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java b/client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java
new file mode 100644
index 0000000000..1309155443
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/notification/VNotification.java
@@ -0,0 +1,458 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.notification;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.Iterator;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.KeyCodes;
+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.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.root.VRoot;
+
+public class VNotification extends VOverlay {
+
+ 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 = "v-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 static final ArrayList<VNotification> notifications = new ArrayList<VNotification>();
+
+ 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;
+ private static final int TOUCH_DEVICE_IDLE_DELAY = 1000;
+
+ public static final String ATTRIBUTE_NOTIFICATION_STYLE = "style";
+ public static final String ATTRIBUTE_NOTIFICATION_CAPTION = "caption";
+ public static final String ATTRIBUTE_NOTIFICATION_MESSAGE = "message";
+ public static final String ATTRIBUTE_NOTIFICATION_ICON = "icon";
+ public static final String ATTRIBUTE_NOTIFICATION_POSITION = "position";
+ public static final String ATTRIBUTE_NOTIFICATION_DELAY = "delay";
+
+ /**
+ * Default constructor. You should use GWT.create instead.
+ */
+ public VNotification() {
+ setStyleName(STYLENAME);
+ sinkEvents(Event.ONCLICK);
+ DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE);
+ }
+
+ /**
+ * @deprecated Use static {@link #createNotification(int)} instead to enable
+ * GWT deferred binding.
+ *
+ * @param delayMsec
+ */
+ @Deprecated
+ public VNotification(int delayMsec) {
+ this();
+ this.delayMsec = delayMsec;
+ if (BrowserInfo.get().isTouchDevice()) {
+ new Timer() {
+ @Override
+ public void run() {
+ if (isAttached()) {
+ fade();
+ }
+ }
+ }.schedule(delayMsec + TOUCH_DEVICE_IDLE_DELAY);
+ }
+ }
+
+ /**
+ * @deprecated Use static {@link #createNotification(int, int, int)} instead
+ * to enable GWT deferred binding.
+ *
+ * @param delayMsec
+ * @param fadeMsec
+ * @param startOpacity
+ */
+ @Deprecated
+ public VNotification(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);
+ addStyleDependentName(style);
+ }
+ super.show();
+ notifications.add(this);
+ setPosition(position);
+ sizeOrPositionUpdated();
+ /**
+ * Android 4 fails to render notifications correctly without a little
+ * nudge (#8551)
+ */
+ if (BrowserInfo.get().isAndroid()) {
+ Util.setStyleTemporarily(getElement(), "display", "none");
+ }
+ }
+
+ @Override
+ public void hide() {
+ DOM.removeEventPreview(this);
+ cancelDelay();
+ cancelFade();
+ if (temporaryStyle != null) {
+ removeStyleName(temporaryStyle);
+ removeStyleDependentName(temporaryStyle);
+ temporaryStyle = null;
+ }
+ super.hide();
+ notifications.remove(this);
+ fireEvent(new HideEvent(this));
+ }
+
+ public void fade() {
+ DOM.removeEventPreview(this);
+ cancelDelay();
+ if (fader == null) {
+ 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;
+ }
+ } else if (type == Event.ONKEYDOWN
+ && event.getKeyCode() == KeyCodes.KEY_ESCAPE) {
+ 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 static void showNotification(ApplicationConnection client,
+ final UIDL notification) {
+ boolean onlyPlainText = notification
+ .hasAttribute(VRoot.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED);
+ String html = "";
+ if (notification.hasAttribute(ATTRIBUTE_NOTIFICATION_ICON)) {
+ final String parsedUri = client.translateVaadinUri(notification
+ .getStringAttribute(ATTRIBUTE_NOTIFICATION_ICON));
+ html += "<img src=\"" + Util.escapeAttribute(parsedUri) + "\" />";
+ }
+ if (notification.hasAttribute(ATTRIBUTE_NOTIFICATION_CAPTION)) {
+ String caption = notification
+ .getStringAttribute(ATTRIBUTE_NOTIFICATION_CAPTION);
+ if (onlyPlainText) {
+ caption = Util.escapeHTML(caption);
+ caption = caption.replaceAll("\\n", "<br />");
+ }
+ html += "<h1>" + caption + "</h1>";
+ }
+ if (notification.hasAttribute(ATTRIBUTE_NOTIFICATION_MESSAGE)) {
+ String message = notification
+ .getStringAttribute(ATTRIBUTE_NOTIFICATION_MESSAGE);
+ if (onlyPlainText) {
+ message = Util.escapeHTML(message);
+ message = message.replaceAll("\\n", "<br />");
+ }
+ html += "<p>" + message + "</p>";
+ }
+
+ final String style = notification
+ .hasAttribute(ATTRIBUTE_NOTIFICATION_STYLE) ? notification
+ .getStringAttribute(ATTRIBUTE_NOTIFICATION_STYLE) : null;
+ final int position = notification
+ .getIntAttribute(ATTRIBUTE_NOTIFICATION_POSITION);
+ final int delay = notification
+ .getIntAttribute(ATTRIBUTE_NOTIFICATION_DELAY);
+ createNotification(delay).show(html, position, style);
+ }
+
+ public static VNotification createNotification(int delayMsec) {
+ final VNotification notification = GWT.create(VNotification.class);
+ notification.delayMsec = delayMsec;
+ if (BrowserInfo.get().isTouchDevice()) {
+ new Timer() {
+ @Override
+ public void run() {
+ if (notification.isAttached()) {
+ notification.fade();
+ }
+ }
+ }.schedule(notification.delayMsec + TOUCH_DEVICE_IDLE_DELAY);
+ }
+ return notification;
+ }
+
+ public class HideEvent extends EventObject {
+
+ public HideEvent(Object source) {
+ super(source);
+ }
+ }
+
+ public interface EventListener extends java.util.EventListener {
+ public void notificationHidden(HideEvent event);
+ }
+
+ /**
+ * Moves currently visible notifications to the top of the event preview
+ * stack. Can be called when opening other overlays such as subwindows to
+ * ensure the notifications receive the events they need and don't linger
+ * indefinitely. See #7136.
+ *
+ * TODO Should this be a generic Overlay feature instead?
+ */
+ public static void bringNotificationsToFront() {
+ for (VNotification notification : notifications) {
+ DOM.removeEventPreview(notification);
+ DOM.addEventPreview(notification);
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java
new file mode 100644
index 0000000000..a4dd72906d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupBaseConnector.java
@@ -0,0 +1,94 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.optiongroup;
+
+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.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.nativebutton.VNativeButton;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+public abstract class OptionGroupBaseConnector extends AbstractFieldConnector
+ implements Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ // Save details
+ getWidget().client = client;
+ getWidget().paintableId = uidl.getId();
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().selectedKeys = uidl.getStringArrayVariableAsSet("selected");
+
+ getWidget().readonly = isReadOnly();
+ getWidget().disabled = !isEnabled();
+ getWidget().multiselect = "multi".equals(uidl
+ .getStringAttribute("selectmode"));
+ getWidget().immediate = getState().isImmediate();
+ getWidget().nullSelectionAllowed = uidl
+ .getBooleanAttribute("nullselect");
+ getWidget().nullSelectionItemAvailable = uidl
+ .getBooleanAttribute("nullselectitem");
+
+ if (uidl.hasAttribute("cols")) {
+ getWidget().cols = uidl.getIntAttribute("cols");
+ }
+ if (uidl.hasAttribute("rows")) {
+ getWidget().rows = uidl.getIntAttribute("rows");
+ }
+
+ final UIDL ops = uidl.getChildUIDL(0);
+
+ if (getWidget().getColumns() > 0) {
+ getWidget().container.setWidth(getWidget().getColumns() + "em");
+ if (getWidget().container != getWidget().optionsContainer) {
+ getWidget().optionsContainer.setWidth("100%");
+ }
+ }
+
+ getWidget().buildOptions(ops);
+
+ if (uidl.getBooleanAttribute("allownewitem")) {
+ if (getWidget().newItemField == null) {
+ getWidget().newItemButton = new VNativeButton();
+ getWidget().newItemButton.setText("+");
+ getWidget().newItemButton.addClickHandler(getWidget());
+ getWidget().newItemField = new VTextField();
+ getWidget().newItemField.addKeyPressHandler(getWidget());
+ }
+ getWidget().newItemField.setEnabled(!getWidget().disabled
+ && !getWidget().readonly);
+ getWidget().newItemButton.setEnabled(!getWidget().disabled
+ && !getWidget().readonly);
+
+ if (getWidget().newItemField == null
+ || getWidget().newItemField.getParent() != getWidget().container) {
+ getWidget().container.add(getWidget().newItemField);
+ getWidget().container.add(getWidget().newItemButton);
+ final int w = getWidget().container.getOffsetWidth()
+ - getWidget().newItemButton.getOffsetWidth();
+ getWidget().newItemField.setWidth(Math.max(w, 0) + "px");
+ }
+ } else if (getWidget().newItemField != null) {
+ getWidget().container.remove(getWidget().newItemField);
+ getWidget().container.remove(getWidget().newItemButton);
+ }
+
+ getWidget().setTabIndex(
+ uidl.hasAttribute("tabindex") ? uidl
+ .getIntAttribute("tabindex") : 0);
+
+ }
+
+ @Override
+ public VOptionGroupBase getWidget() {
+ return (VOptionGroupBase) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java
new file mode 100644
index 0000000000..caf85348d4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/OptionGroupConnector.java
@@ -0,0 +1,67 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.optiongroup;
+
+import java.util.ArrayList;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.EventId;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.ui.OptionGroup;
+
+@Connect(OptionGroup.class)
+public class OptionGroupConnector extends OptionGroupBaseConnector {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().htmlContentAllowed = uidl
+ .hasAttribute(VOptionGroup.HTML_CONTENT_ALLOWED);
+
+ super.updateFromUIDL(uidl, client);
+
+ getWidget().sendFocusEvents = client.hasEventListeners(this,
+ EventId.FOCUS);
+ getWidget().sendBlurEvents = client.hasEventListeners(this,
+ EventId.BLUR);
+
+ if (getWidget().focusHandlers != null) {
+ for (HandlerRegistration reg : getWidget().focusHandlers) {
+ reg.removeHandler();
+ }
+ getWidget().focusHandlers.clear();
+ getWidget().focusHandlers = null;
+
+ for (HandlerRegistration reg : getWidget().blurHandlers) {
+ reg.removeHandler();
+ }
+ getWidget().blurHandlers.clear();
+ getWidget().blurHandlers = null;
+ }
+
+ if (getWidget().sendFocusEvents || getWidget().sendBlurEvents) {
+ getWidget().focusHandlers = new ArrayList<HandlerRegistration>();
+ getWidget().blurHandlers = new ArrayList<HandlerRegistration>();
+
+ // add focus and blur handlers to checkboxes / radio buttons
+ for (Widget wid : getWidget().panel) {
+ if (wid instanceof CheckBox) {
+ getWidget().focusHandlers.add(((CheckBox) wid)
+ .addFocusHandler(getWidget()));
+ getWidget().blurHandlers.add(((CheckBox) wid)
+ .addBlurHandler(getWidget()));
+ }
+ }
+ }
+ }
+
+ @Override
+ public VOptionGroup getWidget() {
+ return (VOptionGroup) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java
new file mode 100644
index 0000000000..a6cdf7e888
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroup.java
@@ -0,0 +1,202 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.optiongroup;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.Focusable;
+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.shared.EventId;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.checkbox.VCheckBox;
+
+public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
+ BlurHandler {
+
+ public static final String HTML_CONTENT_ALLOWED = "usehtml";
+
+ public static final String CLASSNAME = "v-select-optiongroup";
+
+ public static final String ATTRIBUTE_OPTION_DISABLED = "disabled";
+
+ protected final Panel panel;
+
+ private final Map<CheckBox, String> optionsToKeys;
+
+ protected boolean sendFocusEvents = false;
+ protected boolean sendBlurEvents = false;
+ protected List<HandlerRegistration> focusHandlers = null;
+ protected List<HandlerRegistration> blurHandlers = null;
+
+ private final LoadHandler iconLoadHandler = new LoadHandler() {
+ @Override
+ public void onLoad(LoadEvent event) {
+ Util.notifyParentOfSizeChange(VOptionGroup.this, true);
+ }
+ };
+
+ /**
+ * used to check whether a blur really was a blur of the complete
+ * optiongroup: if a control inside this optiongroup gains focus right after
+ * blur of another control inside this optiongroup (meaning: if onFocus
+ * fires after onBlur has fired), the blur and focus won't be sent to the
+ * server side as only a focus change inside this optiongroup occured
+ */
+ private boolean blurOccured = false;
+
+ protected boolean htmlContentAllowed = false;
+
+ public VOptionGroup() {
+ super(CLASSNAME);
+ panel = (Panel) optionsContainer;
+ optionsToKeys = new HashMap<CheckBox, String>();
+ }
+
+ /*
+ * 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;
+
+ String itemHtml = opUidl.getStringAttribute("caption");
+ if (!htmlContentAllowed) {
+ itemHtml = Util.escapeHTML(itemHtml);
+ }
+
+ String icon = opUidl.getStringAttribute("icon");
+ if (icon != null && icon.length() != 0) {
+ String iconUrl = client.translateVaadinUri(icon);
+ itemHtml = "<img src=\"" + iconUrl + "\" class=\""
+ + Icon.CLASSNAME + "\" alt=\"\" />" + itemHtml;
+ }
+
+ if (isMultiselect()) {
+ op = new VCheckBox();
+ op.setHTML(itemHtml);
+ } else {
+ op = new RadioButton(paintableId, itemHtml, true);
+ op.setStyleName("v-radiobutton");
+ }
+
+ if (icon != null && icon.length() != 0) {
+ Util.sinkOnloadForImages(op.getElement());
+ op.addHandler(iconLoadHandler, LoadEvent.getType());
+ }
+
+ op.addStyleName(CLASSNAME_OPTION);
+ op.setValue(opUidl.getBooleanAttribute("selected"));
+ boolean enabled = !opUidl
+ .getBooleanAttribute(ATTRIBUTE_OPTION_DISABLED)
+ && !isReadonly() && !isDisabled();
+ op.setEnabled(enabled);
+ setStyleName(op.getElement(),
+ ApplicationConnection.DISABLED_CLASSNAME, !enabled);
+ op.addClickHandler(this);
+ optionsToKeys.put(op, opUidl.getStringAttribute("key"));
+ panel.add(op);
+ }
+ }
+
+ @Override
+ protected String[] getSelectedItems() {
+ return selectedKeys.toArray(new String[selectedKeys.size()]);
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ super.onClick(event);
+ if (event.getSource() instanceof CheckBox) {
+ final boolean selected = ((CheckBox) event.getSource()).getValue();
+ final String key = optionsToKeys.get(event.getSource());
+ if (!isMultiselect()) {
+ selectedKeys.clear();
+ }
+ if (selected) {
+ selectedKeys.add(key);
+ } else {
+ selectedKeys.remove(key);
+ }
+ client.updateVariable(paintableId, "selected", getSelectedItems(),
+ isImmediate());
+ }
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ for (Iterator<Widget> iterator = panel.iterator(); iterator.hasNext();) {
+ FocusWidget widget = (FocusWidget) iterator.next();
+ widget.setTabIndex(tabIndex);
+ }
+ }
+
+ @Override
+ public void focus() {
+ Iterator<Widget> iterator = panel.iterator();
+ if (iterator.hasNext()) {
+ ((Focusable) iterator.next()).setFocus(true);
+ }
+ }
+
+ @Override
+ public void onFocus(FocusEvent arg0) {
+ if (!blurOccured) {
+ // no blur occured before this focus event
+ // panel was blurred => fire the event to the server side if
+ // requested by server side
+ if (sendFocusEvents) {
+ client.updateVariable(paintableId, EventId.FOCUS, "", true);
+ }
+ } else {
+ // blur occured before this focus event
+ // another control inside the panel (checkbox / radio box) was
+ // blurred => do not fire the focus and set blurOccured to false, so
+ // blur will not be fired, too
+ blurOccured = false;
+ }
+ }
+
+ @Override
+ public void onBlur(BlurEvent arg0) {
+ blurOccured = true;
+ if (sendBlurEvents) {
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ // check whether blurOccured still is true and then send the
+ // event out to the server
+ if (blurOccured) {
+ client.updateVariable(paintableId, EventId.BLUR, "",
+ true);
+ blurOccured = false;
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java
new file mode 100644
index 0000000000..d128d7deb1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/optiongroup/VOptionGroupBase.java
@@ -0,0 +1,171 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.optiongroup;
+
+import java.util.Set;
+
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.nativebutton.VNativeButton;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+public abstract class VOptionGroupBase extends Composite implements Field,
+ ClickHandler, ChangeHandler, KeyPressHandler, Focusable {
+
+ public static final String CLASSNAME_OPTION = "v-select-option";
+
+ protected ApplicationConnection client;
+
+ protected String paintableId;
+
+ protected Set<String> selectedKeys;
+
+ protected boolean immediate;
+
+ protected boolean multiselect;
+
+ protected boolean disabled;
+
+ protected boolean readonly;
+
+ protected int cols = 0;
+
+ protected int rows = 0;
+
+ protected boolean nullSelectionAllowed = true;
+
+ protected 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
+ */
+ protected final Panel container;
+
+ protected VTextField newItemField;
+
+ protected VNativeButton newItemButton;
+
+ public VOptionGroupBase(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 VOptionGroupBase(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;
+ }
+
+ abstract protected void setTabIndex(int tabIndex);
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (event.getSource() == newItemButton
+ && !newItemField.getText().equals("")) {
+ client.updateVariable(paintableId, "newitem",
+ newItemField.getText(), true);
+ newItemField.setText("");
+ }
+ }
+
+ @Override
+ public void onChange(ChangeEvent event) {
+ if (multiselect) {
+ client.updateVariable(paintableId, "selected", getSelectedItems(),
+ immediate);
+ } else {
+ client.updateVariable(paintableId, "selected", new String[] { ""
+ + getSelectedItem() }, immediate);
+ }
+ }
+
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ if (event.getSource() == newItemField
+ && event.getCharCode() == KeyCodes.KEY_ENTER) {
+ newItemButton.click();
+ }
+ }
+
+ protected abstract void buildOptions(UIDL uidl);
+
+ protected abstract String[] getSelectedItems();
+
+ protected String getSelectedItem() {
+ final String[] sel = getSelectedItems();
+ if (sel.length > 0) {
+ return sel[0];
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java
new file mode 100644
index 0000000000..e1bf217691
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java
@@ -0,0 +1,322 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+import java.util.List;
+
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.AlignmentInfo;
+import com.vaadin.shared.ui.LayoutClickRpc;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractLayoutConnector;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.layout.ComponentConnectorLayoutSlot;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+
+public abstract class AbstractOrderedLayoutConnector extends
+ AbstractLayoutConnector implements DirectionalManagedLayout {
+
+ AbstractOrderedLayoutServerRpc rpc;
+
+ private LayoutClickEventHandler clickEventHandler = new LayoutClickEventHandler(
+ this) {
+
+ @Override
+ protected ComponentConnector getChildComponent(Element element) {
+ return Util.getConnectorForElement(getConnection(), getWidget(),
+ element);
+ }
+
+ @Override
+ protected LayoutClickRpc getLayoutClickRPC() {
+ return rpc;
+ };
+
+ };
+
+ @Override
+ public void init() {
+ super.init();
+ rpc = RpcProxy.create(AbstractOrderedLayoutServerRpc.class, this);
+ getLayoutManager().registerDependency(this,
+ getWidget().spacingMeasureElement);
+ }
+
+ @Override
+ public void onUnregister() {
+ LayoutManager lm = getLayoutManager();
+
+ VMeasuringOrderedLayout layout = getWidget();
+ lm.unregisterDependency(this, layout.spacingMeasureElement);
+
+ // Unregister child caption listeners
+ for (ComponentConnector child : getChildComponents()) {
+ VLayoutSlot slot = layout.getSlotForChild(child.getWidget());
+ slot.setCaption(null);
+ }
+ }
+
+ @Override
+ public AbstractOrderedLayoutState getState() {
+ return (AbstractOrderedLayoutState) super.getState();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ VMeasuringOrderedLayout layout = getWidget();
+ if (VCaption.isNeeded(component.getState())) {
+ VLayoutSlot layoutSlot = layout.getSlotForChild(component
+ .getWidget());
+ VCaption caption = layoutSlot.getCaption();
+ if (caption == null) {
+ caption = new VCaption(component, getConnection());
+
+ Widget widget = component.getWidget();
+
+ layout.setCaption(widget, caption);
+ }
+ caption.updateCaption();
+ } else {
+ layout.setCaption(component.getWidget(), null);
+ getLayoutManager().setNeedsLayout(this);
+ }
+ }
+
+ @Override
+ public VMeasuringOrderedLayout getWidget() {
+ return (VMeasuringOrderedLayout) super.getWidget();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ VMeasuringOrderedLayout layout = getWidget();
+
+ for (ComponentConnector child : getChildComponents()) {
+ VLayoutSlot slot = layout.getSlotForChild(child.getWidget());
+
+ AlignmentInfo alignment = new AlignmentInfo(getState()
+ .getChildData().get(child).getAlignmentBitmask());
+ slot.setAlignment(alignment);
+
+ double expandRatio = getState().getChildData().get(child)
+ .getExpandRatio();
+ slot.setExpandRatio(expandRatio);
+ }
+
+ layout.updateMarginStyleNames(new VMarginInfo(getState()
+ .getMarginsBitmask()));
+
+ layout.updateSpacingStyleName(getState().isSpacing());
+
+ getLayoutManager().setNeedsLayout(this);
+ }
+
+ private int getSizeForInnerSize(int size, boolean isVertical) {
+ LayoutManager layoutManager = getLayoutManager();
+ Element element = getWidget().getElement();
+ if (isVertical) {
+ return size + layoutManager.getBorderHeight(element)
+ + layoutManager.getPaddingHeight(element);
+ } else {
+ return size + layoutManager.getBorderWidth(element)
+ + layoutManager.getPaddingWidth(element);
+ }
+ }
+
+ private static String getSizeProperty(boolean isVertical) {
+ return isVertical ? "height" : "width";
+ }
+
+ private boolean isUndefinedInDirection(boolean isVertical) {
+ if (isVertical) {
+ return isUndefinedHeight();
+ } else {
+ return isUndefinedWidth();
+ }
+ }
+
+ private int getInnerSizeInDirection(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager().getInnerHeight(getWidget().getElement());
+ } else {
+ return getLayoutManager().getInnerWidth(getWidget().getElement());
+ }
+ }
+
+ private void layoutPrimaryDirection() {
+ VMeasuringOrderedLayout layout = getWidget();
+ boolean isVertical = layout.isVertical;
+ boolean isUndefined = isUndefinedInDirection(isVertical);
+
+ int startPadding = getStartPadding(isVertical);
+ int endPadding = getEndPadding(isVertical);
+ int spacingSize = getSpacingInDirection(isVertical);
+ int allocatedSize;
+
+ if (isUndefined) {
+ allocatedSize = -1;
+ } else {
+ allocatedSize = getInnerSizeInDirection(isVertical);
+ }
+
+ allocatedSize = layout.layoutPrimaryDirection(spacingSize,
+ allocatedSize, startPadding, endPadding);
+
+ Style ownStyle = getWidget().getElement().getStyle();
+ if (isUndefined) {
+ int outerSize = getSizeForInnerSize(allocatedSize, isVertical);
+ ownStyle.setPropertyPx(getSizeProperty(isVertical), outerSize);
+ reportUndefinedSize(outerSize, isVertical);
+ } else {
+ ownStyle.setProperty(getSizeProperty(isVertical),
+ getDefinedSize(isVertical));
+ }
+ }
+
+ private void reportUndefinedSize(int outerSize, boolean isVertical) {
+ if (isVertical) {
+ getLayoutManager().reportOuterHeight(this, outerSize);
+ } else {
+ getLayoutManager().reportOuterWidth(this, outerSize);
+ }
+ }
+
+ private int getSpacingInDirection(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager().getOuterHeight(
+ getWidget().spacingMeasureElement);
+ } else {
+ return getLayoutManager().getOuterWidth(
+ getWidget().spacingMeasureElement);
+ }
+ }
+
+ private void layoutSecondaryDirection() {
+ VMeasuringOrderedLayout layout = getWidget();
+ boolean isVertical = layout.isVertical;
+ boolean isUndefined = isUndefinedInDirection(!isVertical);
+
+ int startPadding = getStartPadding(!isVertical);
+ int endPadding = getEndPadding(!isVertical);
+
+ int allocatedSize;
+ if (isUndefined) {
+ allocatedSize = -1;
+ } else {
+ allocatedSize = getInnerSizeInDirection(!isVertical);
+ }
+
+ allocatedSize = layout.layoutSecondaryDirection(allocatedSize,
+ startPadding, endPadding);
+
+ Style ownStyle = getWidget().getElement().getStyle();
+
+ if (isUndefined) {
+ int outerSize = getSizeForInnerSize(allocatedSize,
+ !getWidget().isVertical);
+ ownStyle.setPropertyPx(getSizeProperty(!getWidget().isVertical),
+ outerSize);
+ reportUndefinedSize(outerSize, !isVertical);
+ } else {
+ ownStyle.setProperty(getSizeProperty(!getWidget().isVertical),
+ getDefinedSize(!getWidget().isVertical));
+ }
+ }
+
+ private String getDefinedSize(boolean isVertical) {
+ if (isVertical) {
+ return getState().getHeight();
+ } else {
+ return getState().getWidth();
+ }
+ }
+
+ private int getStartPadding(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager().getPaddingTop(getWidget().getElement());
+ } else {
+ return getLayoutManager().getPaddingLeft(getWidget().getElement());
+ }
+ }
+
+ private int getEndPadding(boolean isVertical) {
+ if (isVertical) {
+ return getLayoutManager()
+ .getPaddingBottom(getWidget().getElement());
+ } else {
+ return getLayoutManager().getPaddingRight(getWidget().getElement());
+ }
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ if (getWidget().isVertical) {
+ layoutSecondaryDirection();
+ } else {
+ layoutPrimaryDirection();
+ }
+ }
+
+ @Override
+ public void layoutVertically() {
+ if (getWidget().isVertical) {
+ layoutPrimaryDirection();
+ } else {
+ layoutSecondaryDirection();
+ }
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+ List<ComponentConnector> previousChildren = event.getOldChildren();
+ int currentIndex = 0;
+ VMeasuringOrderedLayout layout = getWidget();
+
+ for (ComponentConnector child : getChildComponents()) {
+ Widget childWidget = child.getWidget();
+ VLayoutSlot slot = layout.getSlotForChild(childWidget);
+
+ if (childWidget.getParent() != layout) {
+ // If the child widget was previously attached to another
+ // AbstractOrderedLayout a slot might be found that belongs to
+ // another AbstractOrderedLayout. In this case we discard it and
+ // create a new slot.
+ slot = new ComponentConnectorLayoutSlot(getWidget()
+ .getStylePrimaryName(), child, this);
+ }
+ layout.addOrMove(slot, currentIndex++);
+ if (child.isRelativeWidth()) {
+ slot.getWrapperElement().getStyle().setWidth(100, Unit.PCT);
+ }
+ }
+
+ for (ComponentConnector child : previousChildren) {
+ if (child.getParent() != this) {
+ // Remove slot if the connector is no longer a child of this
+ // layout
+ layout.removeSlotForWidget(child.getWidget());
+ }
+ }
+
+ };
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java
new file mode 100644
index 0000000000..459bd474d1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/HorizontalLayoutConnector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.ui.HorizontalLayout;
+
+@Connect(value = HorizontalLayout.class, loadStyle = LoadStyle.EAGER)
+public class HorizontalLayoutConnector extends AbstractOrderedLayoutConnector {
+
+ @Override
+ public VHorizontalLayout getWidget() {
+ return (VHorizontalLayout) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java
new file mode 100644
index 0000000000..5bf377642e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VHorizontalLayout.java
@@ -0,0 +1,14 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+public class VHorizontalLayout extends VMeasuringOrderedLayout {
+
+ public static final String CLASSNAME = "v-horizontallayout";
+
+ public VHorizontalLayout() {
+ super(CLASSNAME, false);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java
new file mode 100644
index 0000000000..4bb1c66e86
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VMeasuringOrderedLayout.java
@@ -0,0 +1,241 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+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.Node;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+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.google.gwt.user.client.ui.WidgetCollection;
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.layout.VLayoutSlot;
+
+public class VMeasuringOrderedLayout extends ComplexPanel {
+
+ final boolean isVertical;
+
+ final DivElement spacingMeasureElement;
+
+ private Map<Widget, VLayoutSlot> widgetToSlot = new HashMap<Widget, VLayoutSlot>();
+
+ protected VMeasuringOrderedLayout(String className, boolean isVertical) {
+ DivElement element = Document.get().createDivElement();
+ setElement(element);
+
+ spacingMeasureElement = Document.get().createDivElement();
+ Style spacingStyle = spacingMeasureElement.getStyle();
+ spacingStyle.setPosition(Position.ABSOLUTE);
+ getElement().appendChild(spacingMeasureElement);
+
+ setStyleName(className);
+ this.isVertical = isVertical;
+ }
+
+ public void addOrMove(VLayoutSlot layoutSlot, int index) {
+ Widget widget = layoutSlot.getWidget();
+ Element wrapperElement = layoutSlot.getWrapperElement();
+
+ Element containerElement = getElement();
+ Node childAtIndex = containerElement.getChild(index);
+ if (childAtIndex != wrapperElement) {
+ // Insert at correct location not attached or at wrong location
+ containerElement.insertBefore(wrapperElement, childAtIndex);
+ insert(widget, wrapperElement, index, false);
+ }
+
+ widgetToSlot.put(widget, layoutSlot);
+ }
+
+ private void togglePrefixedStyleName(String name, boolean enabled) {
+ if (enabled) {
+ addStyleDependentName(name);
+ } else {
+ removeStyleDependentName(name);
+ }
+ }
+
+ void updateMarginStyleNames(VMarginInfo marginInfo) {
+ togglePrefixedStyleName("margin-top", marginInfo.hasTop());
+ togglePrefixedStyleName("margin-right", marginInfo.hasRight());
+ togglePrefixedStyleName("margin-bottom", marginInfo.hasBottom());
+ togglePrefixedStyleName("margin-left", marginInfo.hasLeft());
+ }
+
+ void updateSpacingStyleName(boolean spacingEnabled) {
+ String styleName = getStylePrimaryName();
+ if (spacingEnabled) {
+ spacingMeasureElement.addClassName(styleName + "-spacing-on");
+ spacingMeasureElement.removeClassName(styleName + "-spacing-off");
+ } else {
+ spacingMeasureElement.removeClassName(styleName + "-spacing-on");
+ spacingMeasureElement.addClassName(styleName + "-spacing-off");
+ }
+ }
+
+ public void removeSlotForWidget(Widget widget) {
+ VLayoutSlot slot = getSlotForChild(widget);
+ VCaption caption = slot.getCaption();
+ if (caption != null) {
+ // Must remove using setCaption to ensure dependencies (layout ->
+ // caption) are unregistered
+ slot.setCaption(null);
+ }
+
+ remove(slot.getWidget());
+ getElement().removeChild(slot.getWrapperElement());
+ widgetToSlot.remove(widget);
+ }
+
+ public VLayoutSlot getSlotForChild(Widget widget) {
+ return widgetToSlot.get(widget);
+ }
+
+ public void setCaption(Widget child, VCaption caption) {
+ VLayoutSlot slot = getSlotForChild(child);
+
+ if (caption != null) {
+ // Logical attach.
+ getChildren().add(caption);
+ }
+
+ // Physical attach if not null, also removes old caption
+ slot.setCaption(caption);
+
+ if (caption != null) {
+ // Adopt.
+ adopt(caption);
+ }
+ }
+
+ public int layoutPrimaryDirection(int spacingSize, int allocatedSize,
+ int startPadding, int endPadding) {
+ int actuallyAllocated = 0;
+ double totalExpand = 0;
+
+ int childCount = 0;
+ for (Widget child : this) {
+ if (child instanceof VCaption) {
+ continue;
+ }
+ childCount++;
+
+ VLayoutSlot slot = getSlotForChild(child);
+ totalExpand += slot.getExpandRatio();
+
+ if (!slot.isRelativeInDirection(isVertical)) {
+ actuallyAllocated += slot.getUsedSizeInDirection(isVertical);
+ }
+ }
+
+ actuallyAllocated += spacingSize * (childCount - 1);
+
+ if (allocatedSize == -1) {
+ allocatedSize = actuallyAllocated;
+ }
+
+ double unallocatedSpace = Math
+ .max(0, allocatedSize - actuallyAllocated);
+
+ double currentLocation = startPadding;
+
+ WidgetCollection children = getChildren();
+ for (int i = 0; i < children.size(); i++) {
+ Widget child = children.get(i);
+ if (child instanceof VCaption) {
+ continue;
+ }
+
+ VLayoutSlot slot = getSlotForChild(child);
+
+ double childExpandRatio;
+ if (totalExpand == 0) {
+ childExpandRatio = 1d / childCount;
+ } else {
+ childExpandRatio = slot.getExpandRatio() / totalExpand;
+ }
+
+ double extraPixels = unallocatedSpace * childExpandRatio;
+ double endLocation = currentLocation + extraPixels;
+ if (!slot.isRelativeInDirection(isVertical)) {
+ endLocation += slot.getUsedSizeInDirection(isVertical);
+ }
+
+ /*
+ * currentLocation and allocatedSpace are used with full precision
+ * to avoid missing pixels in the end. The pixel dimensions passed
+ * to the DOM are still rounded. Otherwise e.g. 10.5px start
+ * position + 10.5px space might be cause the component to go 1px
+ * beyond the edge as the effect of the browser's rounding may cause
+ * something similar to 11px + 11px.
+ *
+ * It's most efficient to use doubles all the way because native
+ * javascript emulates other number types using doubles.
+ */
+ double roundedLocation = Math.round(currentLocation);
+
+ /*
+ * Space is calculated as the difference between rounded start and
+ * end locations. Just rounding the space would cause e.g. 10.5px +
+ * 10.5px = 21px -> 11px + 11px = 22px but in this way we get 11px +
+ * 10px = 21px.
+ */
+ double roundedSpace = Math.round(endLocation) - roundedLocation;
+
+ // Reserve room for the padding if we're at the end
+ double slotEndMargin;
+ if (i == children.size() - 1) {
+ slotEndMargin = endPadding;
+ } else {
+ slotEndMargin = 0;
+ }
+
+ slot.positionInDirection(roundedLocation, roundedSpace,
+ slotEndMargin, isVertical);
+
+ currentLocation = endLocation + spacingSize;
+ }
+
+ return allocatedSize;
+ }
+
+ public int layoutSecondaryDirection(int allocatedSize, int startPadding,
+ int endPadding) {
+ int maxSize = 0;
+ for (Widget child : this) {
+ if (child instanceof VCaption) {
+ continue;
+ }
+
+ VLayoutSlot slot = getSlotForChild(child);
+ if (!slot.isRelativeInDirection(!isVertical)) {
+ maxSize = Math.max(maxSize,
+ slot.getUsedSizeInDirection(!isVertical));
+ }
+ }
+
+ if (allocatedSize == -1) {
+ allocatedSize = maxSize;
+ }
+
+ for (Widget child : this) {
+ if (child instanceof VCaption) {
+ continue;
+ }
+
+ VLayoutSlot slot = getSlotForChild(child);
+ slot.positionInDirection(startPadding, allocatedSize, endPadding,
+ !isVertical);
+ }
+
+ return allocatedSize;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java
new file mode 100644
index 0000000000..e44c576941
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VVerticalLayout.java
@@ -0,0 +1,14 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+public class VVerticalLayout extends VMeasuringOrderedLayout {
+
+ public static final String CLASSNAME = "v-verticallayout";
+
+ public VVerticalLayout() {
+ super(CLASSNAME, true);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java
new file mode 100644
index 0000000000..a481283156
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/orderedlayout/VerticalLayoutConnector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.orderedlayout;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.ui.VerticalLayout;
+
+@Connect(value = VerticalLayout.class, loadStyle = LoadStyle.EAGER)
+public class VerticalLayoutConnector extends AbstractOrderedLayoutConnector {
+
+ @Override
+ public VVerticalLayout getWidget() {
+ return (VVerticalLayout) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java
new file mode 100644
index 0000000000..fd4761de5e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/panel/PanelConnector.java
@@ -0,0 +1,246 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.panel;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.panel.PanelServerRpc;
+import com.vaadin.shared.ui.panel.PanelState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.ui.Panel;
+
+@Connect(Panel.class)
+public class PanelConnector extends AbstractComponentContainerConnector
+ implements Paintable, SimpleManagedLayout, PostLayoutListener,
+ MayScrollChildren {
+
+ private Integer uidlScrollTop;
+
+ private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+ };
+
+ private Integer uidlScrollLeft;
+
+ private PanelServerRpc rpc;
+
+ @Override
+ public void init() {
+ super.init();
+ rpc = RpcProxy.create(PanelServerRpc.class, this);
+ VPanel panel = getWidget();
+ LayoutManager layoutManager = getLayoutManager();
+
+ layoutManager.registerDependency(this, panel.captionNode);
+ layoutManager.registerDependency(this, panel.bottomDecoration);
+ layoutManager.registerDependency(this, panel.contentNode);
+ }
+
+ @Override
+ public void onUnregister() {
+ VPanel panel = getWidget();
+ LayoutManager layoutManager = getLayoutManager();
+
+ layoutManager.unregisterDependency(this, panel.captionNode);
+ layoutManager.unregisterDependency(this, panel.bottomDecoration);
+ layoutManager.unregisterDependency(this, panel.contentNode);
+ }
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (isRealUpdate(uidl)) {
+
+ // Handle caption displaying and style names, prior generics.
+ // Affects size calculations
+
+ // Restore default stylenames
+ getWidget().contentNode.setClassName(VPanel.CLASSNAME + "-content");
+ getWidget().bottomDecoration.setClassName(VPanel.CLASSNAME
+ + "-deco");
+ getWidget().captionNode.setClassName(VPanel.CLASSNAME + "-caption");
+ boolean hasCaption = false;
+ if (getState().getCaption() != null
+ && !"".equals(getState().getCaption())) {
+ getWidget().setCaption(getState().getCaption());
+ hasCaption = true;
+ } else {
+ getWidget().setCaption("");
+ getWidget().captionNode.setClassName(VPanel.CLASSNAME
+ + "-nocaption");
+ }
+
+ // Add proper stylenames for all elements. This way we can prevent
+ // unwanted CSS selector inheritance.
+ final String captionBaseClass = VPanel.CLASSNAME
+ + (hasCaption ? "-caption" : "-nocaption");
+ final String contentBaseClass = VPanel.CLASSNAME + "-content";
+ final String decoBaseClass = VPanel.CLASSNAME + "-deco";
+ String captionClass = captionBaseClass;
+ String contentClass = contentBaseClass;
+ String decoClass = decoBaseClass;
+ if (getState().hasStyles()) {
+ for (String style : getState().getStyles()) {
+ captionClass += " " + captionBaseClass + "-" + style;
+ contentClass += " " + contentBaseClass + "-" + style;
+ decoClass += " " + decoBaseClass + "-" + style;
+ }
+ }
+ getWidget().captionNode.setClassName(captionClass);
+ getWidget().contentNode.setClassName(contentClass);
+ getWidget().bottomDecoration.setClassName(decoClass);
+
+ getWidget().makeScrollable();
+ }
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ getWidget().client = client;
+ getWidget().id = uidl.getId();
+
+ if (getState().getIcon() != null) {
+ getWidget().setIconUri(getState().getIcon().getURL(), client);
+ } else {
+ getWidget().setIconUri(null, client);
+ }
+
+ getWidget().setErrorIndicatorVisible(
+ null != getState().getErrorMessage());
+
+ // We may have actions attached to this panel
+ if (uidl.getChildCount() > 0) {
+ final int cnt = uidl.getChildCount();
+ for (int i = 0; i < cnt; i++) {
+ UIDL childUidl = uidl.getChildUIDL(i);
+ if (childUidl.getTag().equals("actions")) {
+ if (getWidget().shortcutHandler == null) {
+ getWidget().shortcutHandler = new ShortcutActionHandler(
+ getConnectorId(), client);
+ }
+ getWidget().shortcutHandler.updateActionMap(childUidl);
+ }
+ }
+ }
+
+ if (getState().getScrollTop() != getWidget().scrollTop) {
+ // Sizes are not yet up to date, so changing the scroll position
+ // is deferred to after the layout phase
+ uidlScrollTop = getState().getScrollTop();
+ }
+
+ if (getState().getScrollLeft() != getWidget().scrollLeft) {
+ // Sizes are not yet up to date, so changing the scroll position
+ // is deferred to after the layout phase
+ uidlScrollLeft = getState().getScrollLeft();
+ }
+
+ // And apply tab index
+ getWidget().contentNode.setTabIndex(getState().getTabIndex());
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP: layouts caption, errors etc not rendered in Panel
+ }
+
+ @Override
+ public VPanel getWidget() {
+ return (VPanel) super.getWidget();
+ }
+
+ @Override
+ public void layout() {
+ updateSizes();
+ }
+
+ void updateSizes() {
+ VPanel panel = getWidget();
+
+ LayoutManager layoutManager = getLayoutManager();
+ int top = layoutManager.getOuterHeight(panel.captionNode);
+ int bottom = layoutManager.getInnerHeight(panel.bottomDecoration);
+
+ Style style = panel.getElement().getStyle();
+ panel.captionNode.getParentElement().getStyle()
+ .setMarginTop(-top, Unit.PX);
+ panel.bottomDecoration.getStyle().setMarginBottom(-bottom, Unit.PX);
+ style.setPaddingTop(top, Unit.PX);
+ style.setPaddingBottom(bottom, Unit.PX);
+
+ // Update scroll positions
+ panel.contentNode.setScrollTop(panel.scrollTop);
+ panel.contentNode.setScrollLeft(panel.scrollLeft);
+ // Read actual value back to ensure update logic is correct
+ panel.scrollTop = panel.contentNode.getScrollTop();
+ panel.scrollLeft = panel.contentNode.getScrollLeft();
+ }
+
+ @Override
+ public void postLayout() {
+ VPanel panel = getWidget();
+ if (uidlScrollTop != null) {
+ panel.contentNode.setScrollTop(uidlScrollTop.intValue());
+ // Read actual value back to ensure update logic is correct
+ // TODO Does this trigger reflows?
+ panel.scrollTop = panel.contentNode.getScrollTop();
+ uidlScrollTop = null;
+ }
+
+ if (uidlScrollLeft != null) {
+ panel.contentNode.setScrollLeft(uidlScrollLeft.intValue());
+ // Read actual value back to ensure update logic is correct
+ // TODO Does this trigger reflows?
+ panel.scrollLeft = panel.contentNode.getScrollLeft();
+ uidlScrollLeft = null;
+ }
+ }
+
+ @Override
+ public PanelState getState() {
+ return (PanelState) super.getState();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+ // We always have 1 child, unless the child is hidden
+ Widget newChildWidget = null;
+ if (getChildComponents().size() == 1) {
+ ComponentConnector newChild = getChildComponents().get(0);
+ newChildWidget = newChild.getWidget();
+ }
+
+ getWidget().setWidget(newChildWidget);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java
new file mode 100644
index 0000000000..8764d61c3a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/panel/VPanel.java
@@ -0,0 +1,188 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.panel;
+
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler;
+
+public class VPanel extends SimplePanel implements ShortcutActionHandlerOwner,
+ Focusable {
+
+ public static final String CLASSNAME = "v-panel";
+
+ ApplicationConnection client;
+
+ String id;
+
+ final Element captionNode = DOM.createDiv();
+
+ private final Element captionText = DOM.createSpan();
+
+ private Icon icon;
+
+ final Element bottomDecoration = DOM.createDiv();
+
+ final Element contentNode = DOM.createDiv();
+
+ private Element errorIndicatorElement;
+
+ ShortcutActionHandler shortcutHandler;
+
+ int scrollTop;
+
+ int scrollLeft;
+
+ private TouchScrollHandler touchScrollHandler;
+
+ public VPanel() {
+ 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);
+
+ /*
+ * Make contentNode focusable only by using the setFocus() method. This
+ * behaviour can be changed by invoking setTabIndex() in the serverside
+ * implementation
+ */
+ contentNode.setTabIndex(-1);
+
+ getElement().appendChild(contentNode);
+
+ getElement().appendChild(bottomDecoration);
+ setStyleName(CLASSNAME);
+ DOM.sinkEvents(getElement(), Event.ONKEYDOWN);
+ DOM.sinkEvents(contentNode, Event.ONSCROLL | Event.TOUCHEVENTS);
+
+ contentNode.getStyle().setProperty("position", "relative");
+ getElement().getStyle().setProperty("overflow", "hidden");
+
+ makeScrollable();
+ }
+
+ /**
+ * Sets the keyboard focus on the Panel
+ *
+ * @param focus
+ * Should the panel have focus or not.
+ */
+ public void setFocus(boolean focus) {
+ if (focus) {
+ getContainerElement().focus();
+ } else {
+ getContainerElement().blur();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Focusable#focus()
+ */
+
+ @Override
+ public void focus() {
+ setFocus(true);
+
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ return contentNode;
+ }
+
+ void setCaption(String text) {
+ DOM.setInnerHTML(captionText, text);
+ }
+
+ void setErrorIndicatorVisible(boolean showError) {
+ if (showError) {
+ if (errorIndicatorElement == null) {
+ errorIndicatorElement = DOM.createSpan();
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "v-errorindicator");
+ DOM.sinkEvents(errorIndicatorElement, Event.MOUSEEVENTS);
+ sinkEvents(Event.MOUSEEVENTS);
+ }
+ DOM.insertBefore(captionNode, errorIndicatorElement, captionText);
+ } else if (errorIndicatorElement != null) {
+ DOM.removeChild(captionNode, errorIndicatorElement);
+ errorIndicatorElement = null;
+ }
+ }
+
+ void setIconUri(String iconUri, ApplicationConnection client) {
+ 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);
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(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);
+ }
+ }
+ }
+
+ @Override
+ public ShortcutActionHandler getShortcutActionHandler() {
+ return shortcutHandler;
+ }
+
+ /**
+ * Ensures the panel is scrollable eg. after style name changes
+ */
+ void makeScrollable() {
+ if (touchScrollHandler == null) {
+ touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
+ }
+ touchScrollHandler.addElement(contentNode);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java
new file mode 100644
index 0000000000..55d645f12e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/PasswordFieldConnector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.passwordfield;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ui.textfield.TextFieldConnector;
+import com.vaadin.ui.PasswordField;
+
+@Connect(PasswordField.class)
+public class PasswordFieldConnector extends TextFieldConnector {
+
+ @Override
+ public VPasswordField getWidget() {
+ return (VPasswordField) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java
new file mode 100644
index 0000000000..c160322de5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/passwordfield/VPasswordField.java
@@ -0,0 +1,22 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.passwordfield;
+
+import com.google.gwt.user.client.DOM;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+/**
+ * This class represents a password field.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+public class VPasswordField extends VTextField {
+
+ public VPasswordField() {
+ super(DOM.createInputPassword());
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java
new file mode 100644
index 0000000000..19b35821c7
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/PopupViewConnector.java
@@ -0,0 +1,117 @@
+/*
+ @VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.popupview;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.VCaptionWrapper;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.ui.PopupView;
+
+@Connect(PopupView.class)
+public class PopupViewConnector extends AbstractComponentContainerConnector
+ implements Paintable, PostLayoutListener {
+
+ private boolean centerAfterLayout = false;
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ /**
+ *
+ *
+ * @see com.vaadin.terminal.gwt.client.ComponentConnector#updateFromUIDL(com.vaadin.terminal.gwt.client.UIDL,
+ * com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+ // These are for future server connections
+ getWidget().client = client;
+ getWidget().uidlId = uidl.getId();
+
+ getWidget().hostPopupVisible = uidl
+ .getBooleanVariable("popupVisibility");
+
+ getWidget().setHTML(uidl.getStringAttribute("html"));
+
+ if (uidl.hasAttribute("hideOnMouseOut")) {
+ getWidget().popup.setHideOnMouseOut(uidl
+ .getBooleanAttribute("hideOnMouseOut"));
+ }
+
+ // Render the popup if visible and show it.
+ if (getWidget().hostPopupVisible) {
+ UIDL popupUIDL = uidl.getChildUIDL(0);
+
+ // showPopupOnTop(popup, hostReference);
+ getWidget().preparePopup(getWidget().popup);
+ getWidget().popup.updateFromUIDL(popupUIDL, client);
+ if (getState().hasStyles()) {
+ final StringBuffer styleBuf = new StringBuffer();
+ final String primaryName = getWidget().popup
+ .getStylePrimaryName();
+ styleBuf.append(primaryName);
+ for (String style : getState().getStyles()) {
+ styleBuf.append(" ");
+ styleBuf.append(primaryName);
+ styleBuf.append("-");
+ styleBuf.append(style);
+ }
+ getWidget().popup.setStyleName(styleBuf.toString());
+ } else {
+ getWidget().popup.setStyleName(getWidget().popup
+ .getStylePrimaryName());
+ }
+ getWidget().showPopup(getWidget().popup);
+ centerAfterLayout = true;
+
+ // The popup shouldn't be visible, try to hide it.
+ } else {
+ getWidget().popup.hide();
+ }
+ }// updateFromUIDL
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ if (VCaption.isNeeded(component.getState())) {
+ if (getWidget().popup.captionWrapper != null) {
+ getWidget().popup.captionWrapper.updateCaption();
+ } else {
+ getWidget().popup.captionWrapper = new VCaptionWrapper(
+ component, getConnection());
+ getWidget().popup.setWidget(getWidget().popup.captionWrapper);
+ getWidget().popup.captionWrapper.updateCaption();
+ }
+ } else {
+ if (getWidget().popup.captionWrapper != null) {
+ getWidget().popup
+ .setWidget(getWidget().popup.popupComponentWidget);
+ }
+ }
+ }
+
+ @Override
+ public VPopupView getWidget() {
+ return (VPopupView) super.getWidget();
+ }
+
+ @Override
+ public void postLayout() {
+ if (centerAfterLayout) {
+ centerAfterLayout = false;
+ getWidget().center();
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java
new file mode 100644
index 0000000000..6efcd8f417
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/popupview/VPopupView.java
@@ -0,0 +1,375 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.popupview;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.Focusable;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Label;
+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.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VCaptionWrapper;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.richtextarea.VRichTextArea;
+
+public class VPopupView extends HTML {
+
+ public static final String CLASSNAME = "v-popupview";
+
+ /** For server-client communication */
+ String uidlId;
+ ApplicationConnection client;
+
+ /** This variable helps to communicate popup visibility to the server */
+ boolean hostPopupVisible;
+
+ final CustomPopup popup;
+ private final Label loading = new Label();
+
+ /**
+ * loading constructor
+ */
+ public VPopupView() {
+ super();
+ popup = new CustomPopup();
+
+ setStyleName(CLASSNAME);
+ popup.setStyleName(CLASSNAME + "-popup");
+ loading.setStyleName(CLASSNAME + "-loading");
+
+ setHTML("");
+ popup.setWidget(loading);
+
+ // When we click to open the popup...
+ addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ updateState(true);
+ }
+ });
+
+ // ..and when we close it
+ popup.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ updateState(false);
+ }
+ });
+
+ popup.setAnimationEnabled(true);
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ void preparePopup(final CustomPopup popup) {
+ popup.setVisible(false);
+ popup.show();
+ }
+
+ /**
+ * Determines the correct position for a popup and displays the popup at
+ * that position.
+ *
+ * By default, the popup is shown centered relative to its host component,
+ * ensuring it is visible on the screen if possible.
+ *
+ * Can be overridden to customize the popup position.
+ *
+ * @param popup
+ */
+ protected void showPopup(final CustomPopup popup) {
+ popup.setPopupPosition(0, 0);
+
+ popup.setVisible(true);
+ }
+
+ void center() {
+ 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 = VPopupView.this.getAbsoluteLeft()
+ + VPopupView.this.getOffsetWidth() / 2;
+ int hostVerticalCenter = VPopupView.this.getAbsoluteTop()
+ + VPopupView.this.getOffsetHeight() / 2;
+
+ int left = hostHorizontalCenter - offsetWidth / 2;
+ int top = hostVerticalCenter - offsetHeight / 2;
+
+ // Don't show the popup outside the screen.
+ if ((left + offsetWidth) > windowRight) {
+ left -= (left + offsetWidth) - windowRight;
+ }
+
+ if ((top + offsetHeight) > windowBottom) {
+ top -= (top + offsetHeight) - windowBottom;
+ }
+
+ if (left < 0) {
+ left = 0;
+ }
+
+ if (top < 0) {
+ top = 0;
+ }
+
+ popup.setPopupPosition(left, top);
+ }
+
+ /**
+ * 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();
+ }
+ }-*/;
+
+ /**
+ * This class is only protected to enable overriding showPopup, and is
+ * currently not intended to be extended or otherwise used directly. Its API
+ * (other than it being a VOverlay) is to be considered private and
+ * potentially subject to change.
+ */
+ protected class CustomPopup extends VOverlay {
+
+ private ComponentConnector popupComponentPaintable = null;
+ Widget popupComponentWidget = null;
+ VCaptionWrapper captionWrapper = null;
+
+ private boolean hasHadMouseOver = false;
+ private boolean hideOnMouseOut = true;
+ private final Set<Element> activeChildren = new HashSet<Element>();
+ private boolean hiding = false;
+
+ private ShortcutActionHandler shortcutActionHandler;
+
+ public CustomPopup() {
+ super(true, false, true); // autoHide, not modal, dropshadow
+
+ // Delegate popup keyboard events to the relevant handler. The
+ // events do not propagate automatically because the popup is
+ // directly attached to the RootPanel.
+ addDomHandler(new KeyDownHandler() {
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (shortcutActionHandler != null) {
+ shortcutActionHandler.handleKeyboardEvent(Event
+ .as(event.getNativeEvent()));
+ }
+ }
+ }, KeyDownEvent.getType());
+ }
+
+ // 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;
+ }
+ }
+
+ // Was the TAB key released outside of our popup?
+ if (!eventTargetsPopup && type == Event.ONKEYUP
+ && event.getKeyCode() == KeyCodes.KEY_TAB) {
+ // Should we hide on focus out (mouse out)?
+ if (hideOnMouseOut) {
+ hide();
+ return true;
+ }
+ }
+
+ return super.onEventPreview(event);
+ }
+
+ @Override
+ public void hide(boolean autoClosed) {
+ VConsole.log("Hiding popupview");
+ hiding = true;
+ syncChildren();
+ if (popupComponentWidget != null && popupComponentWidget != loading) {
+ remove(popupComponentWidget);
+ }
+ hasHadMouseOver = false;
+ shortcutActionHandler = null;
+ super.hide(autoClosed);
+ }
+
+ @Override
+ public void show() {
+ hiding = false;
+
+ // Find the shortcut action handler that should handle keyboard
+ // events from the popup. The events do not propagate automatically
+ // because the popup is directly attached to the RootPanel.
+ Widget widget = VPopupView.this;
+ while (shortcutActionHandler == null && widget != null) {
+ if (widget instanceof ShortcutActionHandlerOwner) {
+ shortcutActionHandler = ((ShortcutActionHandlerOwner) widget)
+ .getShortcutActionHandler();
+ }
+ widget = widget.getParent();
+ }
+
+ super.show();
+ }
+
+ /**
+ * Try to sync all known active child widgets to server
+ */
+ public void syncChildren() {
+ // Notify children with focus
+ if ((popupComponentWidget instanceof Focusable)) {
+ ((Focusable) popupComponentWidget).setFocus(false);
+ } else {
+
+ checkForRTE(popupComponentWidget);
+ }
+
+ // Notify children that have used the keyboard
+ for (Element e : activeChildren) {
+ try {
+ nativeBlur(e);
+ } catch (Exception ignored) {
+ }
+ }
+ activeChildren.clear();
+ }
+
+ private void checkForRTE(Widget popupComponentWidget2) {
+ if (popupComponentWidget2 instanceof VRichTextArea) {
+ ((VRichTextArea) popupComponentWidget2)
+ .synchronizeContentToServer();
+ } else if (popupComponentWidget2 instanceof HasWidgets) {
+ HasWidgets hw = (HasWidgets) popupComponentWidget2;
+ Iterator<Widget> iterator = hw.iterator();
+ while (iterator.hasNext()) {
+ checkForRTE(iterator.next());
+ }
+ }
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+
+ popupComponentPaintable = null;
+ popupComponentWidget = null;
+ captionWrapper = null;
+
+ return super.remove(w);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ ComponentConnector newPopupComponent = client.getPaintable(uidl
+ .getChildUIDL(0));
+
+ if (newPopupComponent != popupComponentPaintable) {
+ Widget newWidget = newPopupComponent.getWidget();
+ setWidget(newWidget);
+ popupComponentWidget = newWidget;
+ popupComponentPaintable = newPopupComponent;
+ }
+
+ }
+
+ public void setHideOnMouseOut(boolean hideOnMouseOut) {
+ this.hideOnMouseOut = hideOnMouseOut;
+ }
+
+ /*
+ *
+ * We need a hack make popup act as a child of VPopupView in Vaadin's
+ * 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 VPopupView.this;
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ hiding = false;
+ }
+
+ @Override
+ public Element getContainerElement() {
+ return super.getContainerElement();
+ }
+
+ }// class CustomPopup
+
+}// class VPopupView
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java
new file mode 100644
index 0000000000..cdb0174a49
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/ProgressIndicatorConnector.java
@@ -0,0 +1,60 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.progressindicator;
+
+import com.google.gwt.user.client.DOM;
+import com.vaadin.shared.ui.Connect;
+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.ui.AbstractFieldConnector;
+import com.vaadin.ui.ProgressIndicator;
+
+@Connect(ProgressIndicator.class)
+public class ProgressIndicatorConnector extends AbstractFieldConnector
+ implements Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // Save details
+ getWidget().client = client;
+
+ getWidget().indeterminate = uidl.getBooleanAttribute("indeterminate");
+
+ if (getWidget().indeterminate) {
+ String basename = VProgressIndicator.CLASSNAME + "-indeterminate";
+ getWidget().addStyleName(basename);
+ if (!isEnabled()) {
+ getWidget().addStyleName(basename + "-disabled");
+ } else {
+ getWidget().removeStyleName(basename + "-disabled");
+ }
+ } else {
+ try {
+ final float f = Float.parseFloat(uidl
+ .getStringAttribute("state"));
+ final int size = Math.round(100 * f);
+ DOM.setStyleAttribute(getWidget().indicator, "width", size
+ + "%");
+ } catch (final Exception e) {
+ }
+ }
+
+ if (isEnabled()) {
+ getWidget().interval = uidl.getIntAttribute("pollinginterval");
+ getWidget().poller.scheduleRepeating(getWidget().interval);
+ }
+ }
+
+ @Override
+ public VProgressIndicator getWidget() {
+ return (VProgressIndicator) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java
new file mode 100644
index 0000000000..bc64efb60a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/progressindicator/VProgressIndicator.java
@@ -0,0 +1,71 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.progressindicator;
+
+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.Util;
+
+public class VProgressIndicator extends Widget {
+
+ public static final String CLASSNAME = "v-progressindicator";
+ Element wrapper = DOM.createDiv();
+ Element indicator = DOM.createDiv();
+ protected ApplicationConnection client;
+ protected final Poller poller;
+ protected boolean indeterminate = false;
+ private boolean pollerSuspendedDueDetach;
+ protected int interval;
+
+ public VProgressIndicator() {
+ setElement(DOM.createDiv());
+ getElement().appendChild(wrapper);
+ setStyleName(CLASSNAME);
+ wrapper.appendChild(indicator);
+ indicator.setClassName(CLASSNAME + "-indicator");
+ wrapper.setClassName(CLASSNAME + "-wrapper");
+ poller = new Poller();
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (pollerSuspendedDueDetach) {
+ poller.scheduleRepeating(interval);
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (interval > 0) {
+ 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() {
+ if (!client.hasActiveRequest()
+ && Util.isAttachedAndDisplayed(VProgressIndicator.this)) {
+ client.sendPendingVariableChanges();
+ }
+ }
+
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java
new file mode 100644
index 0000000000..66781eb645
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/RichTextAreaConnector.java
@@ -0,0 +1,73 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.richtextarea;
+
+import com.google.gwt.user.client.Event;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+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.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener;
+import com.vaadin.ui.RichTextArea;
+
+@Connect(value = RichTextArea.class, loadStyle = LoadStyle.LAZY)
+public class RichTextAreaConnector extends AbstractFieldConnector implements
+ Paintable, BeforeShortcutActionListener {
+
+ @Override
+ public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) {
+ getWidget().client = client;
+ getWidget().id = uidl.getId();
+
+ if (uidl.hasVariable("text")) {
+ getWidget().currentValue = uidl.getStringVariable("text");
+ if (getWidget().rta.isAttached()) {
+ getWidget().rta.setHTML(getWidget().currentValue);
+ } else {
+ getWidget().html.setHTML(getWidget().currentValue);
+ }
+ }
+ if (isRealUpdate(uidl)) {
+ getWidget().setEnabled(isEnabled());
+ }
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().setReadOnly(isReadOnly());
+ getWidget().immediate = getState().isImmediate();
+ int newMaxLength = uidl.hasAttribute("maxLength") ? uidl
+ .getIntAttribute("maxLength") : -1;
+ if (newMaxLength >= 0) {
+ if (getWidget().maxLength == -1) {
+ getWidget().keyPressHandler = getWidget().rta
+ .addKeyPressHandler(getWidget());
+ }
+ getWidget().maxLength = newMaxLength;
+ } else if (getWidget().maxLength != -1) {
+ getWidget().getElement().setAttribute("maxlength", "");
+ getWidget().maxLength = -1;
+ getWidget().keyPressHandler.removeHandler();
+ }
+
+ if (uidl.hasAttribute("selectAll")) {
+ getWidget().selectAll();
+ }
+
+ }
+
+ @Override
+ public void onBeforeShortcutAction(Event e) {
+ getWidget().synchronizeContentToServer();
+ }
+
+ @Override
+ public VRichTextArea getWidget() {
+ return (VRichTextArea) super.getWidget();
+ };
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java
new file mode 100644
index 0000000000..f9b399caac
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextArea.java
@@ -0,0 +1,361 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.richtextarea;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.Timer;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.RichTextArea;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+
+/**
+ * This class implements a basic client side rich text editor component.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+public class VRichTextArea extends Composite implements Field, ChangeHandler,
+ BlurHandler, KeyPressHandler, KeyDownHandler, Focusable {
+
+ /**
+ * The input node CSS classname.
+ */
+ public static final String CLASSNAME = "v-richtextarea";
+
+ protected String id;
+
+ protected ApplicationConnection client;
+
+ boolean immediate = false;
+
+ RichTextArea rta;
+
+ private VRichTextToolbar formatter;
+
+ HTML html = new HTML();
+
+ private final FlowPanel fp = new FlowPanel();
+
+ private boolean enabled = true;
+
+ private int extraHorizontalPixels = -1;
+ private int extraVerticalPixels = -1;
+
+ int maxLength = -1;
+
+ private int toolbarNaturalWidth = 500;
+
+ HandlerRegistration keyPressHandler;
+
+ private ShortcutActionHandlerOwner hasShortcutActionHandler;
+
+ String currentValue = "";
+
+ private boolean readOnly = false;
+
+ public VRichTextArea() {
+ createRTAComponents();
+ fp.add(formatter);
+ fp.add(rta);
+
+ initWidget(fp);
+ setStyleName(CLASSNAME);
+
+ TouchScrollDelegate.enableTouchScrolling(html, html.getElement());
+ }
+
+ private void createRTAComponents() {
+ rta = new RichTextArea();
+ rta.setWidth("100%");
+ rta.addBlurHandler(this);
+ rta.addKeyDownHandler(this);
+ formatter = new VRichTextToolbar(rta);
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (this.enabled != enabled) {
+ // rta.setEnabled(enabled);
+ swapEditableArea();
+ this.enabled = enabled;
+ }
+ }
+
+ /**
+ * Swaps html to rta and visa versa.
+ */
+ private void swapEditableArea() {
+ if (html.isAttached()) {
+ fp.remove(html);
+ if (BrowserInfo.get().isWebkit()) {
+ fp.remove(formatter);
+ createRTAComponents(); // recreate new RTA to bypass #5379
+ fp.add(formatter);
+ }
+ rta.setHTML(currentValue);
+ fp.add(rta);
+ } else {
+ html.setHTML(currentValue);
+ fp.remove(rta);
+ fp.add(html);
+ }
+ }
+
+ void selectAll() {
+ /*
+ * There is a timing issue if trying to select all immediately on first
+ * render. Simple deferred command is not enough. Using Timer with
+ * moderated timeout. If this appears to fail on many (most likely slow)
+ * environments, consider increasing the timeout.
+ *
+ * FF seems to require the most time to stabilize its RTA. On Vaadin
+ * tiergarden test machines, 200ms was not enough always (about 50%
+ * success rate) - 300 ms was 100% successful. This however was not
+ * enough on a sluggish old non-virtualized XP test machine. A bullet
+ * proof solution would be nice, GWT 2.1 might however solve these. At
+ * least setFocus has a workaround for this kind of issue.
+ */
+ new Timer() {
+ @Override
+ public void run() {
+ rta.getFormatter().selectAll();
+ }
+ }.schedule(320);
+ }
+
+ void setReadOnly(boolean b) {
+ if (isReadOnly() != b) {
+ swapEditableArea();
+ readOnly = b;
+ }
+ // reset visibility in case enabled state changed and the formatter was
+ // recreated
+ formatter.setVisible(!readOnly);
+ }
+
+ private boolean isReadOnly() {
+ return readOnly;
+ }
+
+ // TODO is this really used, or does everything go via onBlur() only?
+ @Override
+ public void onChange(ChangeEvent event) {
+ synchronizeContentToServer();
+ }
+
+ /**
+ * Method is public to let popupview force synchronization on close.
+ */
+ public void synchronizeContentToServer() {
+ if (client != null && id != null) {
+ final String html = rta.getHTML();
+ if (!html.equals(currentValue)) {
+ client.updateVariable(id, "text", html, immediate);
+ currentValue = html;
+ }
+ }
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ synchronizeContentToServer();
+ // TODO notify possible server side blur/focus listeners
+ }
+
+ /**
+ * @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 {
+ /*
+ * The formatter height will be initially calculated wrong so we
+ * delay the height setting so the DOM has had time to stabilize.
+ */
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ int editorHeight = getOffsetHeight()
+ - getExtraVerticalPixels()
+ - formatter.getOffsetHeight();
+ if (editorHeight < 0) {
+ editorHeight = 0;
+ }
+ 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);
+ }
+ }
+
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ if (maxLength >= 0) {
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ if (rta.getHTML().length() > maxLength) {
+ rta.setHTML(rta.getHTML().substring(0, maxLength));
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ // delegate to closest shortcut action handler
+ // throw event from the iframe forward to the shortcuthandler
+ ShortcutActionHandler shortcutHandler = getShortcutHandlerOwner()
+ .getShortcutActionHandler();
+ if (shortcutHandler != null) {
+ shortcutHandler
+ .handleKeyboardEvent(com.google.gwt.user.client.Event
+ .as(event.getNativeEvent()),
+ ConnectorMap.get(client).getConnector(this));
+ }
+ }
+
+ private ShortcutActionHandlerOwner getShortcutHandlerOwner() {
+ if (hasShortcutActionHandler == null) {
+ Widget parent = getParent();
+ while (parent != null) {
+ if (parent instanceof ShortcutActionHandlerOwner) {
+ break;
+ }
+ parent = parent.getParent();
+ }
+ hasShortcutActionHandler = (ShortcutActionHandlerOwner) parent;
+ }
+ return hasShortcutActionHandler;
+ }
+
+ @Override
+ public int getTabIndex() {
+ return rta.getTabIndex();
+ }
+
+ @Override
+ public void setAccessKey(char key) {
+ rta.setAccessKey(key);
+ }
+
+ @Override
+ public void setFocus(boolean focused) {
+ /*
+ * Similar issue as with selectAll. Focusing must happen before possible
+ * selectall, so keep the timeout here lower.
+ */
+ new Timer() {
+
+ @Override
+ public void run() {
+ rta.setFocus(true);
+ }
+ }.schedule(300);
+ }
+
+ @Override
+ public void setTabIndex(int index) {
+ rta.setTabIndex(index);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$Strings.properties b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$Strings.properties
new file mode 100644
index 0000000000..363b704584
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar$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/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java
new file mode 100644
index 0000000000..d86c7a4bf5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/VRichTextToolbar.java
@@ -0,0 +1,464 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/*
+ * 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.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.i18n.client.Constants;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Image;
+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;
+
+/**
+ * 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 VRichTextToolbar extends Composite {
+
+ /**
+ * This {@link ClientBundle} is used for all the button icons. Using a
+ * 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 ClientBundle {
+
+ ImageResource bold();
+
+ ImageResource createLink();
+
+ ImageResource hr();
+
+ ImageResource indent();
+
+ ImageResource insertImage();
+
+ ImageResource italic();
+
+ ImageResource justifyCenter();
+
+ ImageResource justifyLeft();
+
+ ImageResource justifyRight();
+
+ ImageResource ol();
+
+ ImageResource outdent();
+
+ ImageResource removeFormat();
+
+ ImageResource removeLink();
+
+ ImageResource strikeThrough();
+
+ ImageResource subscript();
+
+ ImageResource superscript();
+
+ ImageResource ul();
+
+ ImageResource 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 EventHandler class to avoid exposing event methods on the
+ * RichTextToolbar itself.
+ */
+ private class EventHandler implements ClickHandler, ChangeHandler,
+ KeyUpHandler {
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onChange(ChangeEvent event) {
+ Object sender = event.getSource();
+ 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);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onClick(ClickEvent event) {
+ Object sender = event.getSource();
+ 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();
+ }
+ }
+
+ @Override
+ public void onKeyUp(KeyUpEvent event) {
+ if (event.getSource() == richText) {
+ // We use the RichTextArea's onKeyUp event to update the toolbar
+ // status. This will catch any cases where the user moves the
+ // cursor 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 EventHandler handler = new EventHandler();
+
+ private final RichTextArea richText;
+ @SuppressWarnings("deprecation")
+ private final RichTextArea.BasicFormatter basic;
+ @SuppressWarnings("deprecation")
+ 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
+ */
+ @SuppressWarnings("deprecation")
+ public VRichTextToolbar(RichTextArea richText) {
+ this.richText = richText;
+ basic = richText.getBasicFormatter();
+ extended = richText.getExtendedFormatter();
+
+ outer.add(topPanel);
+ outer.add(bottomPanel);
+ topPanel.setStyleName("gwt-RichTextToolbar-top");
+ bottomPanel.setStyleName("gwt-RichTextToolbar-bottom");
+
+ 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 handlers for updating status, so don't hook
+ // them up unless at least basic editing is supported.
+ richText.addKeyUpHandler(handler);
+ richText.addClickHandler(handler);
+ }
+ }
+
+ private ListBox createColorList(String caption) {
+ final ListBox lb = new ListBox();
+ lb.addChangeHandler(handler);
+ 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");
+ lb.setTabIndex(-1);
+ return lb;
+ }
+
+ private ListBox createFontList() {
+ final ListBox lb = new ListBox();
+ lb.addChangeHandler(handler);
+ lb.setVisibleItemCount(1);
+
+ lb.addItem(strings.font(), "");
+ lb.addItem(strings.normal(), "inherit");
+ 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");
+ lb.setTabIndex(-1);
+ return lb;
+ }
+
+ private ListBox createFontSizes() {
+ final ListBox lb = new ListBox();
+ lb.addChangeHandler(handler);
+ 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());
+ lb.setTabIndex(-1);
+ return lb;
+ }
+
+ private PushButton createPushButton(ImageResource img, String tip) {
+ final PushButton pb = new PushButton(new Image(img));
+ pb.addClickHandler(handler);
+ pb.setTitle(tip);
+ pb.setTabIndex(-1);
+ return pb;
+ }
+
+ private ToggleButton createToggleButton(ImageResource img, String tip) {
+ final ToggleButton tb = new ToggleButton(new Image(img));
+ tb.addClickHandler(handler);
+ tb.setTitle(tip);
+ tb.setTabIndex(-1);
+ return tb;
+ }
+
+ /**
+ * Updates the status of all the stateful buttons.
+ */
+ @SuppressWarnings("deprecation")
+ 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/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif
new file mode 100644
index 0000000000..ddfc1cea2c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif
new file mode 100644
index 0000000000..7c22eaac68
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif
new file mode 100644
index 0000000000..1a1412fe0e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif
new file mode 100644
index 0000000000..c2f4c8cb21
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif
new file mode 100644
index 0000000000..1629cabb78
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif
new file mode 100644
index 0000000000..2bb89ef189
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png
new file mode 100644
index 0000000000..80728186d8
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif
new file mode 100644
index 0000000000..d507082cf1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif
new file mode 100644
index 0000000000..905421ed76
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif
new file mode 100644
index 0000000000..394ec432a5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif
new file mode 100644
index 0000000000..ffe0e97284
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif
new file mode 100644
index 0000000000..f7d4c4693d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif
new file mode 100644
index 0000000000..bc37a3ed5a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif
new file mode 100644
index 0000000000..892d569384
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif
new file mode 100644
index 0000000000..54f8e4f551
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif
new file mode 100644
index 0000000000..78fd1b5722
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif
new file mode 100644
index 0000000000..cf92c9774f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif
new file mode 100644
index 0000000000..40721a7bca
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif
new file mode 100644
index 0000000000..a7a233c023
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif
new file mode 100644
index 0000000000..58b6fbb816
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif
new file mode 100644
index 0000000000..a6270f6e21
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif
new file mode 100644
index 0000000000..83f1562bcb
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif
new file mode 100644
index 0000000000..06f0200fdd
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java
new file mode 100644
index 0000000000..7b5097ff77
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java
@@ -0,0 +1,431 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.root;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.root.PageClientRpc;
+import com.vaadin.shared.ui.root.RootServerRpc;
+import com.vaadin.shared.ui.root.RootState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent.StateChangeHandler;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+import com.vaadin.terminal.gwt.client.ui.window.WindowConnector;
+import com.vaadin.ui.Root;
+
+@Connect(value = Root.class, loadStyle = LoadStyle.EAGER)
+public class RootConnector extends AbstractComponentContainerConnector
+ implements Paintable, MayScrollChildren {
+
+ private RootServerRpc rpc = RpcProxy.create(RootServerRpc.class, this);
+
+ private HandlerRegistration childStateChangeHandlerRegistration;
+
+ private final StateChangeHandler childStateChangeHandler = new StateChangeHandler() {
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ // TODO Should use a more specific handler that only reacts to
+ // size changes
+ onChildSizeChange();
+ }
+ };
+
+ @Override
+ protected void init() {
+ super.init();
+ registerRpc(PageClientRpc.class, new PageClientRpc() {
+ @Override
+ public void setTitle(String title) {
+ com.google.gwt.user.client.Window.setTitle(title);
+ }
+ });
+ }
+
+ @Override
+ public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) {
+ ConnectorMap paintableMap = ConnectorMap.get(getConnection());
+ getWidget().rendering = true;
+ getWidget().id = getConnectorId();
+ boolean firstPaint = getWidget().connection == null;
+ getWidget().connection = client;
+
+ getWidget().immediate = getState().isImmediate();
+ getWidget().resizeLazy = uidl.hasAttribute(VRoot.RESIZE_LAZY);
+ String newTheme = uidl.getStringAttribute("theme");
+ if (getWidget().theme != null && !newTheme.equals(getWidget().theme)) {
+ // Complete page refresh is needed due css can affect layout
+ // calculations etc
+ getWidget().reloadHostPage();
+ } else {
+ getWidget().theme = newTheme;
+ }
+ // this also implicitly removes old styles
+ String styles = "";
+ styles += getWidget().getStylePrimaryName() + " ";
+ if (getState().hasStyles()) {
+ for (String style : getState().getStyles()) {
+ styles += style + " ";
+ }
+ }
+ if (!client.getConfiguration().isStandalone()) {
+ styles += getWidget().getStylePrimaryName() + "-embedded";
+ }
+ getWidget().setStyleName(styles.trim());
+
+ getWidget().makeScrollable();
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ // 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 = client.translateVaadinUri(open
+ .getStringAttribute("src"));
+ final String target = open.getStringAttribute("name");
+ if (target == null) {
+ // source will be opened to this browser window, but we may have
+ // to finish rendering this window in case this is a download
+ // (and window stays open).
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ VRoot.goTo(url);
+ }
+ });
+ } else if ("_self".equals(target)) {
+ // This window is closing (for sure). Only other opens are
+ // relevant in this change. See #3558, #2144
+ isClosed = true;
+ VRoot.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, something else will be opened to this
+ // browser view
+ getWidget().rendering = false;
+ return;
+ }
+
+ // Handle other UIDL children
+ UIDL childUidl;
+ while ((childUidl = uidl.getChildUIDL(childIndex++)) != null) {
+ String tag = childUidl.getTag().intern();
+ if (tag == "actions") {
+ if (getWidget().actionHandler == null) {
+ getWidget().actionHandler = new ShortcutActionHandler(
+ getWidget().id, client);
+ }
+ getWidget().actionHandler.updateActionMap(childUidl);
+ } else if (tag == "notifications") {
+ for (final Iterator<?> it = childUidl.getChildIterator(); it
+ .hasNext();) {
+ final UIDL notification = (UIDL) it.next();
+ VNotification.showNotification(client, notification);
+ }
+ }
+ }
+
+ if (uidl.hasAttribute("focused")) {
+ // set focused component when render phase is finished
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ ComponentConnector paintable = (ComponentConnector) uidl
+ .getPaintableAttribute("focused", getConnection());
+
+ final Widget toBeFocused = paintable.getWidget();
+ /*
+ * Two types of Widgets can be focused, either implementing
+ * GWT HasFocus of a thinner Vaadin specific Focusable
+ * interface.
+ */
+ if (toBeFocused instanceof com.google.gwt.user.client.ui.Focusable) {
+ final com.google.gwt.user.client.ui.Focusable toBeFocusedWidget = (com.google.gwt.user.client.ui.Focusable) toBeFocused;
+ toBeFocusedWidget.setFocus(true);
+ } else if (toBeFocused instanceof Focusable) {
+ ((Focusable) toBeFocused).focus();
+ } else {
+ VConsole.log("Could not focus component");
+ }
+ }
+ });
+ }
+
+ // Add window listeners on first paint, to prevent premature
+ // variablechanges
+ if (firstPaint) {
+ Window.addWindowClosingHandler(getWidget());
+ Window.addResizeHandler(getWidget());
+ }
+
+ // finally set scroll position from UIDL
+ if (uidl.hasVariable("scrollTop")) {
+ getWidget().scrollable = true;
+ getWidget().scrollTop = uidl.getIntVariable("scrollTop");
+ DOM.setElementPropertyInt(getWidget().getElement(), "scrollTop",
+ getWidget().scrollTop);
+ getWidget().scrollLeft = uidl.getIntVariable("scrollLeft");
+ DOM.setElementPropertyInt(getWidget().getElement(), "scrollLeft",
+ getWidget().scrollLeft);
+ } else {
+ getWidget().scrollable = false;
+ }
+
+ if (uidl.hasAttribute("scrollTo")) {
+ final ComponentConnector connector = (ComponentConnector) uidl
+ .getPaintableAttribute("scrollTo", getConnection());
+ scrollIntoView(connector);
+ }
+
+ if (uidl.hasAttribute(VRoot.FRAGMENT_VARIABLE)) {
+ getWidget().currentFragment = uidl
+ .getStringAttribute(VRoot.FRAGMENT_VARIABLE);
+ if (!getWidget().currentFragment.equals(History.getToken())) {
+ History.newItem(getWidget().currentFragment, true);
+ }
+ } else {
+ // Initial request for which the server doesn't yet have a fragment
+ // (and haven't shown any interest in getting one)
+ getWidget().currentFragment = History.getToken();
+
+ // Include current fragment in the next request
+ client.updateVariable(getWidget().id, VRoot.FRAGMENT_VARIABLE,
+ getWidget().currentFragment, false);
+ }
+
+ if (firstPaint) {
+ // Queue the initial window size to be sent with the following
+ // request.
+ getWidget().sendClientResized();
+ }
+ getWidget().rendering = false;
+ }
+
+ public void init(String rootPanelId,
+ ApplicationConnection applicationConnection) {
+ DOM.sinkEvents(getWidget().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(getWidget().getElement(), "tabIndex", "1");
+
+ RootPanel root = RootPanel.get(rootPanelId);
+
+ // Remove the v-app-loading or any splash screen added inside the div by
+ // the user
+ root.getElement().setInnerHTML("");
+
+ root.addStyleName("v-theme-"
+ + applicationConnection.getConfiguration().getThemeName());
+
+ root.add(getWidget());
+
+ if (applicationConnection.getConfiguration().isStandalone()) {
+ // set focus to iview element by default to listen possible keyboard
+ // shortcuts. For embedded applications this is unacceptable as we
+ // don't want to steal focus from the main page nor we don't want
+ // side-effects from focusing (scrollIntoView).
+ getWidget().getElement().focus();
+ }
+ }
+
+ private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+
+ };
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP The main view never draws caption for its layout
+ }
+
+ @Override
+ public VRoot getWidget() {
+ return (VRoot) super.getWidget();
+ }
+
+ protected ComponentConnector getContent() {
+ return (ComponentConnector) getState().getContent();
+ }
+
+ protected void onChildSizeChange() {
+ ComponentConnector child = getContent();
+ Style childStyle = child.getWidget().getElement().getStyle();
+ /*
+ * Must set absolute position if the child has relative height and
+ * there's a chance of horizontal scrolling as some browsers will
+ * otherwise not take the scrollbar into account when calculating the
+ * height. Assuming v-view does not have an undefined width for now, see
+ * #8460.
+ */
+ if (child.isRelativeHeight() && !BrowserInfo.get().isIE9()) {
+ childStyle.setPosition(Position.ABSOLUTE);
+ } else {
+ childStyle.clearPosition();
+ }
+ }
+
+ /**
+ * Checks if the given sub window is a child of this Root Connector
+ *
+ * @deprecated Should be replaced by a more generic mechanism for getting
+ * non-ComponentConnector children
+ * @param wc
+ * @return
+ */
+ @Deprecated
+ public boolean hasSubWindow(WindowConnector wc) {
+ return getChildComponents().contains(wc);
+ }
+
+ /**
+ * Return an iterator for current subwindows. This method is meant for
+ * testing purposes only.
+ *
+ * @return
+ */
+ public List<WindowConnector> getSubWindows() {
+ ArrayList<WindowConnector> windows = new ArrayList<WindowConnector>();
+ for (ComponentConnector child : getChildComponents()) {
+ if (child instanceof WindowConnector) {
+ windows.add((WindowConnector) child);
+ }
+ }
+ return windows;
+ }
+
+ @Override
+ public RootState getState() {
+ return (RootState) super.getState();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ ComponentConnector oldChild = null;
+ ComponentConnector newChild = getContent();
+
+ for (ComponentConnector c : event.getOldChildren()) {
+ if (!(c instanceof WindowConnector)) {
+ oldChild = c;
+ break;
+ }
+ }
+
+ if (oldChild != newChild) {
+ if (childStateChangeHandlerRegistration != null) {
+ childStateChangeHandlerRegistration.removeHandler();
+ childStateChangeHandlerRegistration = null;
+ }
+ getWidget().setWidget(newChild.getWidget());
+ childStateChangeHandlerRegistration = newChild
+ .addStateChangeHandler(childStateChangeHandler);
+ // Must handle new child here as state change events are already
+ // fired
+ onChildSizeChange();
+ }
+
+ for (ComponentConnector c : getChildComponents()) {
+ if (c instanceof WindowConnector) {
+ WindowConnector wc = (WindowConnector) c;
+ wc.setWindowOrderAndPosition();
+ }
+ }
+
+ // Close removed sub windows
+ for (ComponentConnector c : event.getOldChildren()) {
+ if (c.getParent() != this && c instanceof WindowConnector) {
+ ((WindowConnector) c).getWidget().hide();
+ }
+ }
+ }
+
+ /**
+ * Tries to scroll the viewport so that the given connector is in view.
+ *
+ * @param componentConnector
+ * The connector which should be visible
+ *
+ */
+ public void scrollIntoView(final ComponentConnector componentConnector) {
+ if (componentConnector == null) {
+ return;
+ }
+
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ componentConnector.getWidget().getElement().scrollIntoView();
+ }
+ });
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java b/client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java
new file mode 100644
index 0000000000..fddb76e755
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/root/VRoot.java
@@ -0,0 +1,461 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.root;
+
+import java.util.ArrayList;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+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.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+/**
+ *
+ */
+public class VRoot extends SimplePanel implements ResizeHandler,
+ Window.ClosingHandler, ShortcutActionHandlerOwner, Focusable {
+
+ public static final String FRAGMENT_VARIABLE = "fragment";
+
+ public static final String BROWSER_HEIGHT_VAR = "browserHeight";
+
+ public static final String BROWSER_WIDTH_VAR = "browserWidth";
+
+ private static final String CLASSNAME = "v-view";
+
+ public static final String NOTIFICATION_HTML_CONTENT_NOT_ALLOWED = "useplain";
+
+ private static int MONITOR_PARENT_TIMER_INTERVAL = 1000;
+
+ String theme;
+
+ String id;
+
+ ShortcutActionHandler actionHandler;
+
+ /*
+ * Last known window size used to detect whether VView should be layouted
+ * again. Detection must check window size, because the VView size might be
+ * fixed and thus not automatically adapt to changed window sizes.
+ */
+ private int windowWidth;
+ private int windowHeight;
+
+ /*
+ * Last know view size used to detect whether new dimensions should be sent
+ * to the server.
+ */
+ private int viewWidth;
+ private int viewHeight;
+
+ ApplicationConnection connection;
+
+ /** Identifies the click event */
+ public static final String CLICK_EVENT_ID = "click";
+
+ /**
+ * Keep track of possible parent size changes when an embedded application.
+ *
+ * Uses {@link #parentWidth} and {@link #parentHeight} as an optimization to
+ * keep track of when there is a real change.
+ */
+ private Timer resizeTimer;
+
+ /** stored width of parent for embedded application auto-resize */
+ private int parentWidth;
+
+ /** stored height of parent for embedded application auto-resize */
+ private int parentHeight;
+
+ int scrollTop;
+
+ int scrollLeft;
+
+ boolean rendering;
+
+ boolean scrollable;
+
+ boolean immediate;
+
+ boolean resizeLazy = false;
+
+ /**
+ * Attribute name for the lazy resize setting .
+ */
+ public static final String RESIZE_LAZY = "rL";
+
+ private HandlerRegistration historyHandlerRegistration;
+
+ private TouchScrollHandler touchScrollHandler;
+
+ /**
+ * The current URI fragment, used to avoid sending updates if nothing has
+ * changed.
+ */
+ String currentFragment;
+
+ /**
+ * Listener for URI fragment changes. Notifies the server of the new value
+ * whenever the value changes.
+ */
+ private final ValueChangeHandler<String> historyChangeHandler = new ValueChangeHandler<String>() {
+
+ @Override
+ public void onValueChange(ValueChangeEvent<String> event) {
+ String newFragment = event.getValue();
+
+ // Send the new fragment to the server if it has changed
+ if (!newFragment.equals(currentFragment) && connection != null) {
+ currentFragment = newFragment;
+ connection.updateVariable(id, FRAGMENT_VARIABLE, newFragment,
+ true);
+ }
+ }
+ };
+
+ private VLazyExecutor delayedResizeExecutor = new VLazyExecutor(200,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ performSizeCheck();
+ }
+
+ });
+
+ public VRoot() {
+ super();
+ setStyleName(CLASSNAME);
+
+ // Allow focusing the view by using the focus() method, the view
+ // should not be in the document focus flow
+ getElement().setTabIndex(-1);
+ makeScrollable();
+ }
+
+ /**
+ * Start to periodically monitor for parent element resizes if embedded
+ * application (e.g. portlet).
+ */
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ if (isMonitoringParentSize()) {
+ resizeTimer = new Timer() {
+
+ @Override
+ public void run() {
+ // trigger check to see if parent size has changed,
+ // recalculate layouts
+ performSizeCheck();
+ resizeTimer.schedule(MONITOR_PARENT_TIMER_INTERVAL);
+ }
+ };
+ resizeTimer.schedule(MONITOR_PARENT_TIMER_INTERVAL);
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ historyHandlerRegistration = History
+ .addValueChangeHandler(historyChangeHandler);
+ currentFragment = History.getToken();
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ historyHandlerRegistration.removeHandler();
+ historyHandlerRegistration = null;
+ }
+
+ /**
+ * Stop monitoring for parent element resizes.
+ */
+
+ @Override
+ protected void onUnload() {
+ if (resizeTimer != null) {
+ resizeTimer.cancel();
+ resizeTimer = null;
+ }
+ super.onUnload();
+ }
+
+ /**
+ * Called when the window or parent div might have been resized.
+ *
+ * This immediately checks the sizes of the window and the parent div (if
+ * monitoring it) and triggers layout recalculation if they have changed.
+ */
+ protected void performSizeCheck() {
+ windowSizeMaybeChanged(Window.getClientWidth(),
+ Window.getClientHeight());
+ }
+
+ /**
+ * Called when the window or parent div might have been resized.
+ *
+ * This immediately checks the sizes of the window and the parent div (if
+ * monitoring it) and triggers layout recalculation if they have changed.
+ *
+ * @param newWindowWidth
+ * The new width of the window
+ * @param newWindowHeight
+ * The new height of the window
+ *
+ * @deprecated use {@link #performSizeCheck()}
+ */
+ @Deprecated
+ protected void windowSizeMaybeChanged(int newWindowWidth,
+ int newWindowHeight) {
+ boolean changed = false;
+ ComponentConnector connector = ConnectorMap.get(connection)
+ .getConnector(this);
+ if (windowWidth != newWindowWidth) {
+ windowWidth = newWindowWidth;
+ changed = true;
+ connector.getLayoutManager().reportOuterWidth(connector,
+ newWindowWidth);
+ VConsole.log("New window width: " + windowWidth);
+ }
+ if (windowHeight != newWindowHeight) {
+ windowHeight = newWindowHeight;
+ changed = true;
+ connector.getLayoutManager().reportOuterHeight(connector,
+ newWindowHeight);
+ VConsole.log("New window height: " + windowHeight);
+ }
+ Element parentElement = getElement().getParentElement();
+ if (isMonitoringParentSize() && parentElement != null) {
+ // check also for parent size changes
+ int newParentWidth = parentElement.getClientWidth();
+ int newParentHeight = parentElement.getClientHeight();
+ if (parentWidth != newParentWidth) {
+ parentWidth = newParentWidth;
+ changed = true;
+ VConsole.log("New parent width: " + parentWidth);
+ }
+ if (parentHeight != newParentHeight) {
+ parentHeight = newParentHeight;
+ changed = true;
+ VConsole.log("New parent height: " + parentHeight);
+ }
+ }
+ if (changed) {
+ /*
+ * If the window size has changed, layout the VView again and send
+ * new size to the server if the size changed. (Just checking VView
+ * size would cause us to ignore cases when a relatively sized VView
+ * should shrink as the content's size is fixed and would thus not
+ * automatically shrink.)
+ */
+ VConsole.log("Running layout functions due to window or parent resize");
+
+ // update size to avoid (most) redundant re-layout passes
+ // there can still be an extra layout recalculation if webkit
+ // overflow fix updates the size in a deferred block
+ if (isMonitoringParentSize() && parentElement != null) {
+ parentWidth = parentElement.getClientWidth();
+ parentHeight = parentElement.getClientHeight();
+ }
+
+ sendClientResized();
+
+ connector.getLayoutManager().layoutNow();
+ }
+ }
+
+ public String getTheme() {
+ return theme;
+ }
+
+ /**
+ * Used to reload host page on theme changes.
+ */
+ static native void reloadHostPage()
+ /*-{
+ $wnd.location.reload();
+ }-*/;
+
+ /**
+ * Returns true if the body is NOT generated, i.e if someone else has made
+ * the page that we're running in. Otherwise we're in charge of the whole
+ * page.
+ *
+ * @return true if we're running embedded
+ */
+ public boolean isEmbedded() {
+ return !getElement().getOwnerDocument().getBody().getClassName()
+ .contains(ApplicationConnection.GENERATED_BODY_CLASSNAME);
+ }
+
+ /**
+ * Returns true if the size of the parent should be checked periodically and
+ * the application should react to its changes.
+ *
+ * @return true if size of parent should be tracked
+ */
+ protected boolean isMonitoringParentSize() {
+ // could also perform a more specific check (Liferay portlet)
+ return isEmbedded();
+ }
+
+ @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);
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.logical.shared.ResizeHandler#onResize(com.google
+ * .gwt.event.logical.shared.ResizeEvent)
+ */
+
+ @Override
+ public void onResize(ResizeEvent event) {
+ triggerSizeChangeCheck();
+ }
+
+ /**
+ * Called when a resize event is received.
+ *
+ * This may trigger a lazy refresh or perform the size check immediately
+ * depending on the browser used and whether the server side requests
+ * resizes to be lazy.
+ */
+ private void triggerSizeChangeCheck() {
+ /*
+ * IE (pre IE9 at least) will give us some false resize events due to
+ * problems with scrollbars. Firefox 3 might also produce some extra
+ * events. We postpone both the re-layouting and the server side event
+ * for a while to deal with these issues.
+ *
+ * We may also postpone these events to avoid slowness when resizing the
+ * browser window. Constantly recalculating the layout causes the resize
+ * operation to be really slow with complex layouts.
+ */
+ boolean lazy = resizeLazy || BrowserInfo.get().isIE8();
+
+ if (lazy) {
+ delayedResizeExecutor.trigger();
+ } else {
+ performSizeCheck();
+ }
+ }
+
+ /**
+ * Send new dimensions to the server.
+ */
+ void sendClientResized() {
+ Element parentElement = getElement().getParentElement();
+ int viewHeight = parentElement.getClientHeight();
+ int viewWidth = parentElement.getClientWidth();
+
+ connection.updateVariable(id, "height", viewHeight, false);
+ connection.updateVariable(id, "width", viewWidth, false);
+
+ int windowWidth = Window.getClientWidth();
+ int windowHeight = Window.getClientHeight();
+
+ connection.updateVariable(id, BROWSER_WIDTH_VAR, windowWidth, false);
+ connection.updateVariable(id, BROWSER_HEIGHT_VAR, windowHeight,
+ immediate);
+ }
+
+ public native static void goTo(String url)
+ /*-{
+ $wnd.location = url;
+ }-*/;
+
+ @Override
+ public void onWindowClosing(Window.ClosingEvent event) {
+ // Change focus on this window in order to ensure that all state is
+ // collected from textfields
+ // TODO this is a naive hack, that only works with text fields and may
+ // cause some odd issues. Should be replaced with a decent solution, see
+ // also related BeforeShortcutActionListener interface. Same interface
+ // might be usable here.
+ VTextField.flushChangesFromFocusedTextField();
+ }
+
+ private native static void loadAppIdListFromDOM(ArrayList<String> list)
+ /*-{
+ var j;
+ for(j in $wnd.vaadin.vaadinConfigurations) {
+ // $entry not needed as function is not exported
+ list.@java.util.Collection::add(Ljava/lang/Object;)(j);
+ }
+ }-*/;
+
+ @Override
+ public ShortcutActionHandler getShortcutActionHandler() {
+ return actionHandler;
+ }
+
+ @Override
+ public void focus() {
+ getElement().focus();
+ }
+
+ /**
+ * Ensures the root is scrollable eg. after style name changes.
+ */
+ void makeScrollable() {
+ if (touchScrollHandler == null) {
+ touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
+ }
+ touchScrollHandler.addElement(getElement());
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java
new file mode 100644
index 0000000000..f8588dbf3f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/slider/SliderConnector.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.slider;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.user.client.Command;
+import com.vaadin.shared.ui.Connect;
+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.ui.AbstractFieldConnector;
+import com.vaadin.ui.Slider;
+
+@Connect(Slider.class)
+public class SliderConnector extends AbstractFieldConnector implements
+ Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ getWidget().client = client;
+ getWidget().id = uidl.getId();
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().immediate = getState().isImmediate();
+ getWidget().disabled = !isEnabled();
+ getWidget().readonly = isReadOnly();
+
+ getWidget().vertical = uidl.hasAttribute("vertical");
+
+ // TODO should style names be used?
+
+ if (getWidget().vertical) {
+ getWidget().addStyleName(VSlider.CLASSNAME + "-vertical");
+ } else {
+ getWidget().removeStyleName(VSlider.CLASSNAME + "-vertical");
+ }
+
+ getWidget().min = uidl.getDoubleAttribute("min");
+ getWidget().max = uidl.getDoubleAttribute("max");
+ getWidget().resolution = uidl.getIntAttribute("resolution");
+ getWidget().value = new Double(uidl.getDoubleVariable("value"));
+
+ getWidget().setFeedbackValue(getWidget().value);
+
+ getWidget().buildBase();
+
+ if (!getWidget().vertical) {
+ // Draw handle with a delay to allow base to gain maximum width
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ getWidget().buildHandle();
+ getWidget().setValue(getWidget().value, false);
+ }
+ });
+ } else {
+ getWidget().buildHandle();
+ getWidget().setValue(getWidget().value, false);
+ }
+ }
+
+ @Override
+ public VSlider getWidget() {
+ return (VSlider) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java b/client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java
new file mode 100644
index 0000000000..06608c95fe
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/slider/VSlider.java
@@ -0,0 +1,530 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+//
+package com.vaadin.terminal.gwt.client.ui.slider;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.KeyCodes;
+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.HTML;
+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.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.SimpleFocusablePanel;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+
+public class VSlider extends SimpleFocusablePanel implements Field,
+ ContainerResizedListener {
+
+ public static final String CLASSNAME = "v-slider";
+
+ /**
+ * Minimum size (width or height, depending on orientation) of the slider
+ * base.
+ */
+ private static final int MIN_SIZE = 50;
+
+ ApplicationConnection client;
+
+ String id;
+
+ boolean immediate;
+ boolean disabled;
+ boolean readonly;
+
+ private int acceleration = 1;
+ double min;
+ double max;
+ int resolution;
+ Double value;
+ boolean vertical;
+
+ private final HTML feedback = new HTML("", false);
+ private final VOverlay feedbackPopup = new VOverlay(true, false, true) {
+
+ @Override
+ public void show() {
+ super.show();
+ updateFeedbackPosition();
+ }
+ };
+
+ /* 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;
+
+ private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ updateValueToServer();
+ acceleration = 1;
+ }
+ });
+
+ public VSlider() {
+ super();
+
+ 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");
+
+ sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS
+ | Event.FOCUSEVENTS | Event.TOUCHEVENTS);
+
+ feedbackPopup.addStyleName(CLASSNAME + "-feedback");
+ feedbackPopup.setWidget(feedback);
+ }
+
+ void setFeedbackValue(double value) {
+ String currentValue = "" + value;
+ if (resolution == 0) {
+ currentValue = "" + new Double(value).intValue();
+ }
+ feedback.setText(currentValue);
+ }
+
+ private void updateFeedbackPosition() {
+ if (vertical) {
+ feedbackPopup.setPopupPosition(
+ DOM.getAbsoluteLeft(handle) + handle.getOffsetWidth(),
+ DOM.getAbsoluteTop(handle) + handle.getOffsetHeight() / 2
+ - feedbackPopup.getOffsetHeight() / 2);
+ } else {
+ feedbackPopup.setPopupPosition(
+ DOM.getAbsoluteLeft(handle) + handle.getOffsetWidth() / 2
+ - feedbackPopup.getOffsetWidth() / 2,
+ DOM.getAbsoluteTop(handle)
+ - feedbackPopup.getOffsetHeight());
+ }
+ }
+
+ void buildBase() {
+ final String styleAttribute = vertical ? "height" : "width";
+ final String oppositeStyleAttribute = vertical ? "width" : "height";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ // clear unnecessary opposite style attribute
+ DOM.setStyleAttribute(base, oppositeStyleAttribute, "");
+
+ 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");
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ 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);
+ }
+ }
+ });
+ }
+
+ // TODO attach listeners for focusing and arrow keys
+ }
+
+ void buildHandle() {
+ final String handleAttribute = vertical ? "marginTop" : "marginLeft";
+ final String oppositeHandleAttribute = vertical ? "marginLeft"
+ : "marginTop";
+
+ DOM.setStyleAttribute(handle, handleAttribute, "0");
+
+ // clear unnecessary opposite handle attribute
+ DOM.setStyleAttribute(handle, oppositeHandleAttribute, "");
+
+ // Restore visibility
+ DOM.setStyleAttribute(handle, "visibility", "visible");
+
+ }
+
+ void setValue(Double value, boolean updateToServer) {
+ if (value == null) {
+ return;
+ }
+
+ if (value < min) {
+ value = min;
+ } else if (value > max) {
+ value = 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) {
+ p = range - p;
+ }
+ final double pos = p;
+
+ DOM.setStyleAttribute(handle, styleAttribute, (Math.round(pos)) + "px");
+
+ // Update value
+ this.value = new Double(v);
+ setFeedbackValue(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 if (DOM.eventGetType(event) == Event.MOUSEEVENTS) {
+ processBaseEvent(event);
+ } else if ((BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYPRESS)
+ || (!BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYDOWN)) {
+
+ if (handleNavigation(event.getKeyCode(), event.getCtrlKey(),
+ event.getShiftKey())) {
+
+ feedbackPopup.show();
+
+ delayedValueUpdater.trigger();
+
+ DOM.eventPreventDefault(event);
+ DOM.eventCancelBubble(event, true);
+ }
+ } else if (targ.equals(getElement())
+ && DOM.eventGetType(event) == Event.ONFOCUS) {
+ feedbackPopup.show();
+ } else if (targ.equals(getElement())
+ && DOM.eventGetType(event) == Event.ONBLUR) {
+ feedbackPopup.hide();
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
+ feedbackPopup.show();
+ }
+ if (Util.isTouchEvent(event)) {
+ event.preventDefault(); // avoid simulated events
+ event.stopPropagation();
+ }
+ }
+
+ private void processMouseWheelEvent(final Event event) {
+ final int dir = DOM.eventGetMouseWheelVelocityY(event);
+
+ if (dir < 0) {
+ increaseValue(false);
+ } else {
+ decreaseValue(false);
+ }
+
+ delayedValueUpdater.trigger();
+
+ DOM.eventPreventDefault(event);
+ DOM.eventCancelBubble(event, true);
+ }
+
+ private void processHandleEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ case Event.ONTOUCHSTART:
+ if (!disabled && !readonly) {
+ focus();
+ feedbackPopup.show();
+ dragging = true;
+ DOM.setElementProperty(handle, "className", CLASSNAME
+ + "-handle " + CLASSNAME + "-handle-active");
+ DOM.setCapture(getElement());
+ DOM.eventPreventDefault(event); // prevent selecting text
+ DOM.eventCancelBubble(event, true);
+ event.stopPropagation();
+ VConsole.log("Slider move start");
+ }
+ break;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ if (dragging) {
+ VConsole.log("Slider move");
+ setValueByEvent(event, false);
+ updateFeedbackPosition();
+ event.stopPropagation();
+ }
+ break;
+ case Event.ONTOUCHEND:
+ feedbackPopup.hide();
+ case Event.ONMOUSEUP:
+ // feedbackPopup.hide();
+ VConsole.log("Slider move end");
+ dragging = false;
+ DOM.setElementProperty(handle, "className", CLASSNAME + "-handle");
+ DOM.releaseCapture(getElement());
+ setValueByEvent(event, true);
+ event.stopPropagation();
+ 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);
+ }
+ }
+ }
+
+ 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 = getEventPosition(event);
+
+ final int handleSize, baseSize, baseOffset;
+ if (vertical) {
+ handleSize = handle.getOffsetHeight();
+ baseSize = base.getOffsetHeight();
+ baseOffset = base.getAbsoluteTop() - Window.getScrollTop()
+ - handleSize / 2;
+ } else {
+ handleSize = handle.getOffsetWidth();
+ baseSize = base.getOffsetWidth();
+ baseOffset = base.getAbsoluteLeft() - Window.getScrollLeft()
+ + handleSize / 2;
+ }
+
+ if (vertical) {
+ v = ((baseSize - (coord - baseOffset)) / (double) (baseSize - handleSize))
+ * (max - min) + min;
+ } else {
+ v = ((coord - baseOffset) / (double) (baseSize - handleSize))
+ * (max - min) + min;
+ }
+
+ if (v < min) {
+ v = min;
+ } else if (v > max) {
+ v = max;
+ }
+
+ setValue(v, updateToServer);
+ }
+
+ /**
+ * TODO consider extracting touches support to an impl class specific for
+ * webkit (only browser that really supports touches).
+ *
+ * @param event
+ * @return
+ */
+ protected int getEventPosition(Event event) {
+ if (vertical) {
+ return Util.getTouchOrMouseClientY(event);
+ } else {
+ return Util.getTouchOrMouseClientX(event);
+ }
+ }
+
+ @Override
+ public void iLayout() {
+ if (vertical) {
+ setHeight();
+ }
+ // Update handle position
+ setValue(value, false);
+ }
+
+ private void setHeight() {
+ // 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");
+ DOM.setStyleAttribute(base, "overflow", "");
+ }
+
+ private void updateValueToServer() {
+ client.updateVariable(id, "value", value.doubleValue(), immediate);
+ }
+
+ /**
+ * Handles the keyboard events handled by the Slider
+ *
+ * @param event
+ * The keyboard event received
+ * @return true iff the navigation event was handled
+ */
+ public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+
+ // No support for ctrl moving
+ if (ctrl) {
+ return false;
+ }
+
+ if ((keycode == getNavigationUpKey() && vertical)
+ || (keycode == getNavigationRightKey() && !vertical)) {
+ if (shift) {
+ for (int a = 0; a < acceleration; a++) {
+ increaseValue(false);
+ }
+ acceleration++;
+ } else {
+ increaseValue(false);
+ }
+ return true;
+ } else if (keycode == getNavigationDownKey() && vertical
+ || (keycode == getNavigationLeftKey() && !vertical)) {
+ if (shift) {
+ for (int a = 0; a < acceleration; a++) {
+ decreaseValue(false);
+ }
+ acceleration++;
+ } else {
+ decreaseValue(false);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the key that increases the vertical slider. By default it is the up
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that decreases the vertical slider. By default it is the down
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that decreases the horizontal slider. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that increases the horizontal slider. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java
new file mode 100644
index 0000000000..f82718e4ea
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/AbstractSplitPanelConnector.java
@@ -0,0 +1,215 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.dom.client.DomEvent.Type;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc;
+import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState;
+import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent;
+
+public abstract class AbstractSplitPanelConnector extends
+ AbstractComponentContainerConnector implements SimpleManagedLayout {
+
+ private AbstractSplitPanelRpc rpc;
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(AbstractSplitPanelRpc.class, this);
+ // TODO Remove
+ getWidget().client = getConnection();
+
+ getWidget().addHandler(new SplitterMoveHandler() {
+
+ @Override
+ public void splitterMoved(SplitterMoveEvent event) {
+ String position = getWidget().getSplitterPosition();
+ float pos = 0;
+ if (position.indexOf("%") > 0) {
+ // Send % values as a fraction to avoid that the splitter
+ // "jumps" when server responds with the integer pct value
+ // (e.g. dragged 16.6% -> should not jump to 17%)
+ pos = Float.valueOf(position.substring(0,
+ position.length() - 1));
+ } else {
+ pos = Integer.parseInt(position.substring(0,
+ position.length() - 2));
+ }
+
+ rpc.setSplitterPosition(pos);
+ }
+
+ }, SplitterMoveEvent.TYPE);
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // TODO Implement caption handling
+ }
+
+ ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+
+ @Override
+ protected <H extends EventHandler> HandlerRegistration registerHandler(
+ H handler, Type<H> type) {
+ if ((Event.getEventsSunk(getWidget().splitter) & Event
+ .getTypeInt(type.getName())) != 0) {
+ // If we are already sinking the event for the splitter we do
+ // not want to additionally sink it for the root element
+ return getWidget().addHandler(handler, type);
+ } else {
+ return getWidget().addDomHandler(handler, type);
+ }
+ }
+
+ @Override
+ protected boolean shouldFireEvent(DomEvent<?> event) {
+ Element target = event.getNativeEvent().getEventTarget().cast();
+ if (!getWidget().splitter.isOrHasChild(target)) {
+ return false;
+ }
+
+ return super.shouldFireEvent(event);
+ };
+
+ @Override
+ protected Element getRelativeToElement() {
+ return getWidget().splitter;
+ };
+
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.splitterClick(mouseDetails);
+ }
+
+ };
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().setEnabled(isEnabled());
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ if (getState().hasStyles()) {
+ getWidget().componentStyleNames = getState().getStyles();
+ } else {
+ getWidget().componentStyleNames = new LinkedList<String>();
+ }
+
+ // Splitter updates
+ SplitterState splitterState = getState().getSplitterState();
+
+ getWidget().setLocked(splitterState.isLocked());
+ getWidget().setPositionReversed(splitterState.isPositionReversed());
+
+ getWidget().setStylenames();
+
+ getWidget().minimumPosition = splitterState.getMinPosition()
+ + splitterState.getMinPositionUnit();
+
+ getWidget().maximumPosition = splitterState.getMaxPosition()
+ + splitterState.getMaxPositionUnit();
+
+ getWidget().position = splitterState.getPosition()
+ + splitterState.getPositionUnit();
+
+ // This is needed at least for cases like #3458 to take
+ // appearing/disappearing scrollbars into account.
+ getConnection().runDescendentsLayout(getWidget());
+
+ getLayoutManager().setNeedsLayout(this);
+
+ getWidget().makeScrollable();
+ }
+
+ @Override
+ public void layout() {
+ VAbstractSplitPanel splitPanel = getWidget();
+ splitPanel.setSplitPosition(splitPanel.position);
+ splitPanel.updateSizes();
+ // Report relative sizes in other direction for quicker propagation
+ List<ComponentConnector> children = getChildComponents();
+ for (ComponentConnector child : children) {
+ reportOtherDimension(child);
+ }
+ }
+
+ private void reportOtherDimension(ComponentConnector child) {
+ LayoutManager layoutManager = getLayoutManager();
+ if (this instanceof HorizontalSplitPanelConnector) {
+ if (child.isRelativeHeight()) {
+ int height = layoutManager.getInnerHeight(getWidget()
+ .getElement());
+ layoutManager.reportHeightAssignedToRelative(child, height);
+ }
+ } else {
+ if (child.isRelativeWidth()) {
+ int width = layoutManager.getInnerWidth(getWidget()
+ .getElement());
+ layoutManager.reportWidthAssignedToRelative(child, width);
+ }
+ }
+ }
+
+ @Override
+ public VAbstractSplitPanel getWidget() {
+ return (VAbstractSplitPanel) super.getWidget();
+ }
+
+ @Override
+ public AbstractSplitPanelState getState() {
+ return (AbstractSplitPanelState) super.getState();
+ }
+
+ private ComponentConnector getFirstChild() {
+ return (ComponentConnector) getState().getFirstChild();
+ }
+
+ private ComponentConnector getSecondChild() {
+ return (ComponentConnector) getState().getSecondChild();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ Widget newFirstChildWidget = null;
+ if (getFirstChild() != null) {
+ newFirstChildWidget = getFirstChild().getWidget();
+ }
+ getWidget().setFirstWidget(newFirstChildWidget);
+
+ Widget newSecondChildWidget = null;
+ if (getSecondChild() != null) {
+ newSecondChildWidget = getSecondChild().getWidget();
+ }
+ getWidget().setSecondWidget(newSecondChildWidget);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java
new file mode 100644
index 0000000000..8c2c4d24cd
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/HorizontalSplitPanelConnector.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.ui.HorizontalSplitPanel;
+
+@Connect(value = HorizontalSplitPanel.class, loadStyle = LoadStyle.EAGER)
+public class HorizontalSplitPanelConnector extends AbstractSplitPanelConnector {
+
+ @Override
+ public VSplitPanelHorizontal getWidget() {
+ return (VSplitPanelHorizontal) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java
new file mode 100644
index 0000000000..a20c0476a5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VAbstractSplitPanel.java
@@ -0,0 +1,772 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.event.dom.client.TouchCancelEvent;
+import com.google.gwt.event.dom.client.TouchCancelHandler;
+import com.google.gwt.event.dom.client.TouchEndEvent;
+import com.google.gwt.event.dom.client.TouchEndHandler;
+import com.google.gwt.event.dom.client.TouchMoveEvent;
+import com.google.gwt.event.dom.client.TouchMoveHandler;
+import com.google.gwt.event.dom.client.TouchStartEvent;
+import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+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.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.splitpanel.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent;
+
+public class VAbstractSplitPanel extends ComplexPanel {
+
+ private boolean enabled = false;
+
+ public static final String CLASSNAME = "v-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;
+
+ Widget firstChild;
+
+ Widget secondChild;
+
+ private final Element wrapper = DOM.createDiv();
+
+ private final Element firstContainer = DOM.createDiv();
+
+ private final Element secondContainer = DOM.createDiv();
+
+ final Element splitter = DOM.createDiv();
+
+ private boolean resizing;
+
+ private boolean resized = false;
+
+ private int origX;
+
+ private int origY;
+
+ private int origMouseX;
+
+ private int origMouseY;
+
+ private boolean locked = false;
+
+ private boolean positionReversed = false;
+
+ List<String> componentStyleNames = Collections.emptyList();
+
+ private Element draggingCurtain;
+
+ ApplicationConnection client;
+
+ boolean immediate;
+
+ /* The current position of the split handle in either percentages or pixels */
+ String position;
+
+ String maximumPosition;
+
+ String minimumPosition;
+
+ private TouchScrollHandler touchScrollHandler;
+
+ protected Element scrolledContainer;
+
+ protected int origScrollTop;
+
+ public VAbstractSplitPanel() {
+ this(ORIENTATION_HORIZONTAL);
+ }
+
+ public VAbstractSplitPanel(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);
+ sinkEvents(Event.MOUSEEVENTS);
+
+ makeScrollable();
+
+ addDomHandler(new TouchCancelHandler() {
+ @Override
+ public void onTouchCancel(TouchCancelEvent event) {
+ // TODO When does this actually happen??
+ VConsole.log("TOUCH CANCEL");
+ }
+ }, TouchCancelEvent.getType());
+ addDomHandler(new TouchStartHandler() {
+ @Override
+ public void onTouchStart(TouchStartEvent event) {
+ Node target = event.getTouches().get(0).getTarget().cast();
+ if (splitter.isOrHasChild(target)) {
+ onMouseDown(Event.as(event.getNativeEvent()));
+ }
+ }
+ }, TouchStartEvent.getType());
+ addDomHandler(new TouchMoveHandler() {
+ @Override
+ public void onTouchMove(TouchMoveEvent event) {
+ if (resizing) {
+ onMouseMove(Event.as(event.getNativeEvent()));
+ }
+ }
+ }, TouchMoveEvent.getType());
+ addDomHandler(new TouchEndHandler() {
+ @Override
+ public void onTouchEnd(TouchEndEvent event) {
+ if (resizing) {
+ onMouseUp(Event.as(event.getNativeEvent()));
+ }
+ }
+ }, TouchEndEvent.getType());
+
+ }
+
+ 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");
+
+ setStylenames();
+ }
+
+ 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%");
+ }
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ boolean removed = super.remove(w);
+ if (removed) {
+ if (firstChild == w) {
+ firstChild = null;
+ } else {
+ secondChild = null;
+ }
+ }
+ return removed;
+ }
+
+ void setLocked(boolean newValue) {
+ if (locked != newValue) {
+ locked = newValue;
+ splitterSize = -1;
+ setStylenames();
+ }
+ }
+
+ void setPositionReversed(boolean reversed) {
+ if (positionReversed != reversed) {
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ DOM.setStyleAttribute(splitter, "right", "");
+ DOM.setStyleAttribute(splitter, "left", "");
+ } else if (orientation == ORIENTATION_VERTICAL) {
+ DOM.setStyleAttribute(splitter, "top", "");
+ DOM.setStyleAttribute(splitter, "bottom", "");
+ }
+
+ positionReversed = reversed;
+ }
+ }
+
+ /**
+ * Converts given split position string (in pixels or percentage) to a
+ * floating point pixel value.
+ *
+ * @param pos
+ * @return
+ */
+ private float convertToPixels(String pos) {
+ float posAsFloat;
+ if (pos.indexOf("%") > 0) {
+ posAsFloat = Math.round(Float.parseFloat(pos.substring(0,
+ pos.length() - 1))
+ / 100
+ * (orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth()
+ : getOffsetHeight()));
+ } else {
+ posAsFloat = Float.parseFloat(pos.substring(0, pos.length() - 2));
+ }
+ return posAsFloat;
+ }
+
+ /**
+ * Converts given split position string (in pixels or percentage) to a float
+ * percentage value.
+ *
+ * @param pos
+ * @return
+ */
+ private float convertToPercentage(String pos) {
+ if (pos.endsWith("px")) {
+ float pixelPosition = Float.parseFloat(pos.substring(0,
+ pos.length() - 2));
+ int offsetLength = orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth()
+ : getOffsetHeight();
+
+ // Take splitter size into account at the edge
+ if (pixelPosition + getSplitterSize() >= offsetLength) {
+ return 100;
+ }
+
+ return pixelPosition / offsetLength * 100;
+ } else {
+ assert pos.endsWith("%");
+ return Float.parseFloat(pos.substring(0, pos.length() - 1));
+ }
+ }
+
+ /**
+ * Returns the given position clamped to the range between current minimum
+ * and maximum positions.
+ *
+ * TODO Should this be in the connector?
+ *
+ * @param pos
+ * Position of the splitter as a CSS string, either pixels or a
+ * percentage.
+ * @return minimumPosition if pos is less than minimumPosition;
+ * maximumPosition if pos is greater than maximumPosition; pos
+ * otherwise.
+ */
+ private String checkSplitPositionLimits(String pos) {
+ float positionAsFloat = convertToPixels(pos);
+
+ if (maximumPosition != null
+ && convertToPixels(maximumPosition) < positionAsFloat) {
+ pos = maximumPosition;
+ } else if (minimumPosition != null
+ && convertToPixels(minimumPosition) > positionAsFloat) {
+ pos = minimumPosition;
+ }
+ return pos;
+ }
+
+ /**
+ * Converts given string to the same units as the split position is.
+ *
+ * @param pos
+ * position to be converted
+ * @return converted position string
+ */
+ private String convertToPositionUnits(String pos) {
+ if (position.indexOf("%") != -1 && pos.indexOf("%") == -1) {
+ // position is in percentage, pos in pixels
+ pos = convertToPercentage(pos) + "%";
+ } else if (position.indexOf("px") > 0 && pos.indexOf("px") == -1) {
+ // position is in pixels and pos in percentage
+ pos = convertToPixels(pos) + "px";
+ }
+
+ return pos;
+ }
+
+ void setSplitPosition(String pos) {
+ if (pos == null) {
+ return;
+ }
+
+ pos = checkSplitPositionLimits(pos);
+ if (!pos.equals(position)) {
+ position = convertToPositionUnits(pos);
+ }
+
+ // Convert percentage values to pixels
+ if (pos.indexOf("%") > 0) {
+ int size = orientation == ORIENTATION_HORIZONTAL ? getOffsetWidth()
+ : getOffsetHeight();
+ float percentage = Float.parseFloat(pos.substring(0,
+ pos.length() - 1));
+ pos = percentage / 100 * size + "px";
+ }
+
+ String attributeName;
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ if (positionReversed) {
+ attributeName = "right";
+ } else {
+ attributeName = "left";
+ }
+ } else {
+ if (positionReversed) {
+ attributeName = "bottom";
+ } else {
+ attributeName = "top";
+ }
+ }
+
+ Style style = splitter.getStyle();
+ if (!pos.equals(style.getProperty(attributeName))) {
+ style.setProperty(attributeName, pos);
+ updateSizes();
+ }
+ }
+
+ void updateSizes() {
+ if (!isAttached()) {
+ return;
+ }
+
+ 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)
+ || (positionReversed && pixelPosition < 0)) {
+ 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");
+
+ LayoutManager layoutManager = LayoutManager.get(client);
+ ConnectorMap connectorMap = ConnectorMap.get(client);
+ if (firstChild != null) {
+ ComponentConnector connector = connectorMap
+ .getConnector(firstChild);
+ if (connector.isRelativeWidth()) {
+ layoutManager.reportWidthAssignedToRelative(connector,
+ pixelPosition);
+ } else {
+ layoutManager.setNeedsMeasure(connector);
+ }
+ }
+ if (secondChild != null) {
+ ComponentConnector connector = connectorMap
+ .getConnector(secondChild);
+ if (connector.isRelativeWidth()) {
+ layoutManager.reportWidthAssignedToRelative(connector,
+ secondContainerWidth);
+ } else {
+ layoutManager.setNeedsMeasure(connector);
+ }
+ }
+ 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)
+ || (positionReversed && pixelPosition < 0)) {
+ 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");
+
+ layoutManager = LayoutManager.get(client);
+ connectorMap = ConnectorMap.get(client);
+ if (firstChild != null) {
+ ComponentConnector connector = connectorMap
+ .getConnector(firstChild);
+ if (connector.isRelativeHeight()) {
+ layoutManager.reportHeightAssignedToRelative(connector,
+ pixelPosition);
+ } else {
+ layoutManager.setNeedsMeasure(connector);
+ }
+ }
+ if (secondChild != null) {
+ ComponentConnector connector = connectorMap
+ .getConnector(secondChild);
+ if (connector.isRelativeHeight()) {
+ layoutManager.reportHeightAssignedToRelative(connector,
+ secondContainerHeight);
+ } else {
+ layoutManager.setNeedsMeasure(connector);
+ }
+ }
+ break;
+ }
+ }
+
+ void setFirstWidget(Widget w) {
+ if (firstChild != null) {
+ firstChild.removeFromParent();
+ }
+ if (w != null) {
+ super.add(w, firstContainer);
+ }
+ firstChild = w;
+ }
+
+ void setSecondWidget(Widget w) {
+ if (secondChild != null) {
+ secondChild.removeFromParent();
+ }
+ if (w != null) {
+ super.add(w, secondContainer);
+ }
+ secondChild = w;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEMOVE:
+ // case Event.ONTOUCHMOVE:
+ if (resizing) {
+ onMouseMove(event);
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ // case Event.ONTOUCHSTART:
+ onMouseDown(event);
+ break;
+ case Event.ONMOUSEOUT:
+ // Dragging curtain interferes with click events if added in
+ // mousedown so we add it only when needed i.e., if the mouse moves
+ // outside the splitter.
+ if (resizing) {
+ showDraggingCurtain();
+ }
+ break;
+ case Event.ONMOUSEUP:
+ // case Event.ONTOUCHEND:
+ if (resizing) {
+ onMouseUp(event);
+ }
+ break;
+ case Event.ONCLICK:
+ resizing = false;
+ break;
+ }
+ // Only fire click event listeners if the splitter isn't moved
+ if (Util.isTouchEvent(event) || !resized) {
+ super.onBrowserEvent(event);
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
+ // Reset the resized flag after a mouseup has occured so the next
+ // mousedown/mouseup can be interpreted as a click.
+ resized = false;
+ }
+ }
+
+ public void onMouseDown(Event event) {
+ if (locked || !isEnabled()) {
+ return;
+ }
+ final Element trg = event.getEventTarget().cast();
+ if (trg == splitter || trg == DOM.getChild(splitter, 0)) {
+ resizing = true;
+ DOM.setCapture(getElement());
+ origX = DOM.getElementPropertyInt(splitter, "offsetLeft");
+ origY = DOM.getElementPropertyInt(splitter, "offsetTop");
+ origMouseX = Util.getTouchOrMouseClientX(event);
+ origMouseY = Util.getTouchOrMouseClientY(event);
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ public void onMouseMove(Event event) {
+ switch (orientation) {
+ case ORIENTATION_HORIZONTAL:
+ final int x = Util.getTouchOrMouseClientX(event);
+ onHorizontalMouseMove(x);
+ break;
+ case ORIENTATION_VERTICAL:
+ default:
+ final int y = Util.getTouchOrMouseClientY(event);
+ onVerticalMouseMove(y);
+ break;
+ }
+
+ }
+
+ private void onHorizontalMouseMove(int x) {
+ int newX = origX + x - origMouseX;
+ if (newX < 0) {
+ newX = 0;
+ }
+ if (newX + getSplitterSize() > getOffsetWidth()) {
+ newX = getOffsetWidth() - getSplitterSize();
+ }
+
+ if (position.indexOf("%") > 0) {
+ position = convertToPositionUnits(newX + "px");
+ } else {
+ // Reversed position
+ if (positionReversed) {
+ position = (getOffsetWidth() - newX - getSplitterSize()) + "px";
+ } else {
+ position = newX + "px";
+ }
+ }
+
+ if (origX != newX) {
+ resized = true;
+ }
+
+ // Reversed position
+ if (positionReversed) {
+ newX = getOffsetWidth() - newX - getSplitterSize();
+ }
+
+ setSplitPosition(newX + "px");
+ }
+
+ private void onVerticalMouseMove(int y) {
+ int newY = origY + y - origMouseY;
+ if (newY < 0) {
+ newY = 0;
+ }
+
+ if (newY + getSplitterSize() > getOffsetHeight()) {
+ newY = getOffsetHeight() - getSplitterSize();
+ }
+
+ if (position.indexOf("%") > 0) {
+ position = convertToPositionUnits(newY + "px");
+ } else {
+ // Reversed position
+ if (positionReversed) {
+ position = (getOffsetHeight() - newY - getSplitterSize())
+ + "px";
+ } else {
+ position = newY + "px";
+ }
+ }
+
+ if (origY != newY) {
+ resized = true;
+ }
+
+ // Reversed position
+ if (positionReversed) {
+ newY = getOffsetHeight() - newY - getSplitterSize();
+ }
+
+ setSplitPosition(newY + "px");
+ }
+
+ public void onMouseUp(Event event) {
+ DOM.releaseCapture(getElement());
+ hideDraggingCurtain();
+ resizing = false;
+ if (!Util.isTouchEvent(event)) {
+ onMouseMove(event);
+ }
+ fireEvent(new SplitterMoveEvent(this));
+ }
+
+ public interface SplitterMoveHandler extends EventHandler {
+ public void splitterMoved(SplitterMoveEvent event);
+
+ public static class SplitterMoveEvent extends
+ GwtEvent<SplitterMoveHandler> {
+
+ public static final Type<SplitterMoveHandler> TYPE = new Type<SplitterMoveHandler>();
+
+ private Widget splitPanel;
+
+ public SplitterMoveEvent(Widget splitPanel) {
+ this.splitPanel = splitPanel;
+ }
+
+ @Override
+ public com.google.gwt.event.shared.GwtEvent.Type<SplitterMoveHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(SplitterMoveHandler handler) {
+ handler.splitterMoved(this);
+ }
+
+ }
+ }
+
+ String getSplitterPosition() {
+ return position;
+ }
+
+ /**
+ * Used in FF to avoid losing mouse capture when pointer is moved on an
+ * iframe.
+ */
+ private void showDraggingCurtain() {
+ if (!isDraggingCurtainRequired()) {
+ return;
+ }
+ 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", ""
+ + VOverlay.Z_INDEX);
+
+ DOM.appendChild(wrapper, draggingCurtain);
+ }
+ }
+
+ /**
+ * A dragging curtain is required in Gecko and Webkit.
+ *
+ * @return true if the browser requires a dragging curtain
+ */
+ private boolean isDraggingCurtainRequired() {
+ return (BrowserInfo.get().isGecko() || BrowserInfo.get().isWebkit());
+ }
+
+ /**
+ * Hides dragging curtain
+ */
+ private void hideDraggingCurtain() {
+ if (draggingCurtain != null) {
+ DOM.removeChild(wrapper, 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;
+ }
+
+ void setStylenames() {
+ final String splitterClass = CLASSNAME
+ + (orientation == ORIENTATION_HORIZONTAL ? "-hsplitter"
+ : "-vsplitter");
+ final String firstContainerClass = CLASSNAME + "-first-container";
+ final String secondContainerClass = CLASSNAME + "-second-container";
+ final String lockedSuffix = locked ? "-locked" : "";
+
+ splitter.setClassName(splitterClass + lockedSuffix);
+ firstContainer.setClassName(firstContainerClass);
+ secondContainer.setClassName(secondContainerClass);
+
+ for (String styleName : componentStyleNames) {
+ splitter.addClassName(splitterClass + "-" + styleName
+ + lockedSuffix);
+ firstContainer.addClassName(firstContainerClass + "-" + styleName);
+ secondContainer
+ .addClassName(secondContainerClass + "-" + styleName);
+ }
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Ensures the panels are scrollable eg. after style name changes
+ */
+ void makeScrollable() {
+ if (touchScrollHandler == null) {
+ touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
+ }
+ touchScrollHandler.addElement(firstContainer);
+ touchScrollHandler.addElement(secondContainer);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java
new file mode 100644
index 0000000000..9048a59d7d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelHorizontal.java
@@ -0,0 +1,12 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+public class VSplitPanelHorizontal extends VAbstractSplitPanel {
+
+ public VSplitPanelHorizontal() {
+ super(VAbstractSplitPanel.ORIENTATION_HORIZONTAL);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java
new file mode 100644
index 0000000000..d22ebed5d9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VSplitPanelVertical.java
@@ -0,0 +1,12 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+public class VSplitPanelVertical extends VAbstractSplitPanel {
+
+ public VSplitPanelVertical() {
+ super(VAbstractSplitPanel.ORIENTATION_VERTICAL);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java
new file mode 100644
index 0000000000..048136c1c9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/splitpanel/VerticalSplitPanelConnector.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.splitpanel;
+
+import com.google.gwt.core.client.GWT;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.ui.VerticalSplitPanel;
+
+@Connect(value = VerticalSplitPanel.class, loadStyle = LoadStyle.EAGER)
+public class VerticalSplitPanelConnector extends AbstractSplitPanelConnector {
+
+ @Override
+ public VSplitPanelVertical getWidget() {
+ return (VSplitPanelVertical) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java
new file mode 100644
index 0000000000..7721a3d763
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/table/TableConnector.java
@@ -0,0 +1,377 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.table;
+
+import java.util.Iterator;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.ContextMenuDetails;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow;
+
+@Connect(com.vaadin.ui.Table.class)
+public class TableConnector extends AbstractComponentContainerConnector
+ implements Paintable, DirectionalManagedLayout, PostLayoutListener {
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().init(getConnection());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal
+ * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().rendering = true;
+
+ // If a row has an open context menu, it will be closed as the row is
+ // detached. Retain a reference here so we can restore the menu if
+ // required.
+ ContextMenuDetails contextMenuBeforeUpdate = getWidget().contextMenu;
+
+ if (uidl.hasAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST)) {
+ getWidget().serverCacheFirst = uidl
+ .getIntAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST);
+ getWidget().serverCacheLast = uidl
+ .getIntAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_LAST);
+ } else {
+ getWidget().serverCacheFirst = -1;
+ getWidget().serverCacheLast = -1;
+ }
+ /*
+ * We need to do this before updateComponent since updateComponent calls
+ * this.setHeight() which will calculate a new body height depending on
+ * the space available.
+ */
+ if (uidl.hasAttribute("colfooters")) {
+ getWidget().showColFooters = uidl.getBooleanAttribute("colfooters");
+ }
+
+ getWidget().tFoot.setVisible(getWidget().showColFooters);
+
+ if (!isRealUpdate(uidl)) {
+ getWidget().rendering = false;
+ return;
+ }
+
+ getWidget().enabled = isEnabled();
+
+ if (BrowserInfo.get().isIE8() && !getWidget().enabled) {
+ /*
+ * The disabled shim will not cover the table body if it is relative
+ * in IE8. See #7324
+ */
+ getWidget().scrollBodyPanel.getElement().getStyle()
+ .setPosition(Position.STATIC);
+ } else if (BrowserInfo.get().isIE8()) {
+ getWidget().scrollBodyPanel.getElement().getStyle()
+ .setPosition(Position.RELATIVE);
+ }
+
+ getWidget().paintableId = uidl.getStringAttribute("id");
+ getWidget().immediate = getState().isImmediate();
+
+ int previousTotalRows = getWidget().totalRows;
+ getWidget().updateTotalRows(uidl);
+ boolean totalRowsChanged = (getWidget().totalRows != previousTotalRows);
+
+ getWidget().updateDragMode(uidl);
+
+ getWidget().updateSelectionProperties(uidl, getState(), isReadOnly());
+
+ if (uidl.hasAttribute("alb")) {
+ getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb");
+ } else {
+ // Need to clear the actions if the action handlers have been
+ // removed
+ getWidget().bodyActionKeys = null;
+ }
+
+ getWidget().setCacheRateFromUIDL(uidl);
+
+ getWidget().recalcWidths = uidl.hasAttribute("recalcWidths");
+ if (getWidget().recalcWidths) {
+ getWidget().tHead.clear();
+ getWidget().tFoot.clear();
+ }
+
+ getWidget().updatePageLength(uidl);
+
+ getWidget().updateFirstVisibleAndScrollIfNeeded(uidl);
+
+ getWidget().showRowHeaders = uidl.getBooleanAttribute("rowheaders");
+ getWidget().showColHeaders = uidl.getBooleanAttribute("colheaders");
+
+ getWidget().updateSortingProperties(uidl);
+
+ boolean keyboardSelectionOverRowFetchInProgress = getWidget()
+ .selectSelectedRows(uidl);
+
+ getWidget().updateActionMap(uidl);
+
+ getWidget().updateColumnProperties(uidl);
+
+ UIDL ac = uidl.getChildByTagName("-ac");
+ if (ac == null) {
+ if (getWidget().dropHandler != null) {
+ // remove dropHandler if not present anymore
+ getWidget().dropHandler = null;
+ }
+ } else {
+ if (getWidget().dropHandler == null) {
+ getWidget().dropHandler = getWidget().new VScrollTableDropHandler();
+ }
+ getWidget().dropHandler.updateAcceptRules(ac);
+ }
+
+ UIDL partialRowAdditions = uidl.getChildByTagName("prows");
+ UIDL partialRowUpdates = uidl.getChildByTagName("urows");
+ if (partialRowUpdates != null || partialRowAdditions != null) {
+ // we may have pending cache row fetch, cancel it. See #2136
+ getWidget().rowRequestHandler.cancel();
+
+ getWidget().updateRowsInBody(partialRowUpdates);
+ getWidget().addAndRemoveRows(partialRowAdditions);
+ } else {
+ UIDL rowData = uidl.getChildByTagName("rows");
+ if (rowData != null) {
+ // we may have pending cache row fetch, cancel it. See #2136
+ getWidget().rowRequestHandler.cancel();
+
+ if (!getWidget().recalcWidths
+ && getWidget().initializedAndAttached) {
+ getWidget().updateBody(rowData,
+ uidl.getIntAttribute("firstrow"),
+ uidl.getIntAttribute("rows"));
+ if (getWidget().headerChangedDuringUpdate) {
+ getWidget().triggerLazyColumnAdjustment(true);
+ } else if (!getWidget().isScrollPositionVisible()
+ || totalRowsChanged
+ || getWidget().lastRenderedHeight != getWidget().scrollBody
+ .getOffsetHeight()) {
+ // webkits may still bug with their disturbing scrollbar
+ // bug, see #3457
+ // Run overflow fix for the scrollable area
+ // #6698 - If there's a scroll going on, don't abort it
+ // by changing overflows as the length of the contents
+ // *shouldn't* have changed (unless the number of rows
+ // or the height of the widget has also changed)
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(getWidget().scrollBodyPanel
+ .getElement());
+ }
+ });
+ }
+ } else {
+ getWidget().initializeRows(uidl, rowData);
+ }
+ }
+ }
+
+ // If a row had an open context menu before the update, and after the
+ // update there's a row with the same key as that row, restore the
+ // context menu. See #8526.
+ showSavedContextMenu(contextMenuBeforeUpdate);
+
+ if (!getWidget().isSelectable()) {
+ getWidget().scrollBody.addStyleName(VScrollTable.CLASSNAME
+ + "-body-noselection");
+ } else {
+ getWidget().scrollBody.removeStyleName(VScrollTable.CLASSNAME
+ + "-body-noselection");
+ }
+
+ getWidget().hideScrollPositionAnnotation();
+
+ // selection is no in sync with server, avoid excessive server visits by
+ // clearing to flag used during the normal operation
+ if (!keyboardSelectionOverRowFetchInProgress) {
+ getWidget().selectionChanged = false;
+ }
+
+ /*
+ * This is called when the Home or page up button has been pressed in
+ * selectable mode and the next selected row was not yet rendered in the
+ * client
+ */
+ if (getWidget().selectFirstItemInNextRender
+ || getWidget().focusFirstItemInNextRender) {
+ getWidget().selectFirstRenderedRowInViewPort(
+ getWidget().focusFirstItemInNextRender);
+ getWidget().selectFirstItemInNextRender = getWidget().focusFirstItemInNextRender = false;
+ }
+
+ /*
+ * This is called when the page down or end button has been pressed in
+ * selectable mode and the next selected row was not yet rendered in the
+ * client
+ */
+ if (getWidget().selectLastItemInNextRender
+ || getWidget().focusLastItemInNextRender) {
+ getWidget().selectLastRenderedRowInViewPort(
+ getWidget().focusLastItemInNextRender);
+ getWidget().selectLastItemInNextRender = getWidget().focusLastItemInNextRender = false;
+ }
+ getWidget().multiselectPending = false;
+
+ if (getWidget().focusedRow != null) {
+ if (!getWidget().focusedRow.isAttached()
+ && !getWidget().rowRequestHandler.isRunning()) {
+ // focused row has been orphaned, can't focus
+ getWidget().focusRowFromBody();
+ }
+ }
+
+ /*
+ * If the server has (re)initialized the rows, our selectionRangeStart
+ * row will point to an index that the server knows nothing about,
+ * causing problems if doing multi selection with shift. The field will
+ * be cleared a little later when the row focus has been restored.
+ * (#8584)
+ */
+ if (uidl.hasAttribute(VScrollTable.ATTRIBUTE_KEY_MAPPER_RESET)
+ && uidl.getBooleanAttribute(VScrollTable.ATTRIBUTE_KEY_MAPPER_RESET)
+ && getWidget().selectionRangeStart != null) {
+ assert !getWidget().selectionRangeStart.isAttached();
+ getWidget().selectionRangeStart = getWidget().focusedRow;
+ }
+
+ getWidget().tabIndex = uidl.hasAttribute("tabindex") ? uidl
+ .getIntAttribute("tabindex") : 0;
+ getWidget().setProperTabIndex();
+
+ getWidget().resizeSortedColumnForSortIndicator();
+
+ // Remember this to detect situations where overflow hack might be
+ // needed during scrolling
+ getWidget().lastRenderedHeight = getWidget().scrollBody
+ .getOffsetHeight();
+
+ getWidget().rendering = false;
+ getWidget().headerChangedDuringUpdate = false;
+
+ }
+
+ @Override
+ public VScrollTable getWidget() {
+ return (VScrollTable) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP, not rendered
+ }
+
+ @Override
+ public void layoutVertically() {
+ getWidget().updateHeight();
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ getWidget().updateWidth();
+ }
+
+ @Override
+ public void postLayout() {
+ VScrollTable table = getWidget();
+ if (table.sizeNeedsInit) {
+ table.sizeInit();
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ getLayoutManager().setNeedsMeasure(TableConnector.this);
+ ServerConnector parent = getParent();
+ if (parent instanceof ComponentConnector) {
+ getLayoutManager().setNeedsMeasure(
+ (ComponentConnector) parent);
+ }
+ getLayoutManager().setNeedsVerticalLayout(
+ TableConnector.this);
+ getLayoutManager().layoutNow();
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().isPropertyReadOnly();
+ }
+
+ @Override
+ public AbstractFieldState getState() {
+ return (AbstractFieldState) super.getState();
+ }
+
+ /**
+ * Shows a saved row context menu if the row for the context menu is still
+ * visible. Does nothing if a context menu has not been saved.
+ *
+ * @param savedContextMenu
+ */
+ public void showSavedContextMenu(ContextMenuDetails savedContextMenu) {
+ if (isEnabled() && savedContextMenu != null) {
+ Iterator<Widget> iterator = getWidget().scrollBody.iterator();
+ while (iterator.hasNext()) {
+ Widget w = iterator.next();
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (row.getKey().equals(savedContextMenu.rowKey)) {
+ getWidget().contextMenu = savedContextMenu;
+ getConnection().getContextMenu().showAt(row,
+ savedContextMenu.left, savedContextMenu.top);
+ }
+ }
+ }
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+
+ TooltipInfo info = null;
+
+ if (element != getWidget().getElement()) {
+ Object node = Util.findWidget(
+ (com.google.gwt.user.client.Element) element,
+ VScrollTableRow.class);
+
+ if (node != null) {
+ VScrollTableRow row = (VScrollTableRow) node;
+ info = row.getTooltip(element);
+ }
+ }
+
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java b/client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java
new file mode 100644
index 0000000000..8a58c28c5b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/table/VScrollTable.java
@@ -0,0 +1,6917 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.table;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+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.dom.client.Touch;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.ui.Action;
+import com.vaadin.terminal.gwt.client.ui.ActionOwner;
+import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TreeAction;
+import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
+import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
+import com.vaadin.terminal.gwt.client.ui.embedded.VEmbedded;
+import com.vaadin.terminal.gwt.client.ui.label.VLabel;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+/**
+ * VScrollTable
+ *
+ * VScrollTable 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 VScrollTableBody object which handles content. To save
+ * some bandwidth and to improve clients responsiveness with loads of data, in
+ * VScrollTableBody all rows are not necessary rendered. There are "spacers" in
+ * VScrollTableBody 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 VScrollTable 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 VScrollTable extends FlowPanel implements HasWidgets,
+ ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable,
+ ActionOwner {
+
+ public enum SelectMode {
+ NONE(0), SINGLE(1), MULTI(2);
+ private int id;
+
+ private SelectMode(int id) {
+ this.id = id;
+ }
+
+ public int getId() {
+ return id;
+ }
+ }
+
+ /**
+ * Tell the client that old keys are no longer valid because the server has
+ * cleared its key map.
+ */
+ public static final String ATTRIBUTE_KEY_MAPPER_RESET = "clearKeyMap";
+
+ private static final String ROW_HEADER_COLUMN_KEY = "0";
+
+ public static final String CLASSNAME = "v-table";
+ public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus";
+
+ public static final String ATTRIBUTE_PAGEBUFFER_FIRST = "pb-ft";
+ public static final String ATTRIBUTE_PAGEBUFFER_LAST = "pb-l";
+
+ public static final String ITEM_CLICK_EVENT_ID = "itemClick";
+ public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick";
+ public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick";
+ public static final String COLUMN_RESIZE_EVENT_ID = "columnResize";
+ public static final String COLUMN_REORDER_EVENT_ID = "columnReorder";
+
+ private static final double CACHE_RATE_DEFAULT = 2;
+
+ /**
+ * The default multi select mode where simple left clicks only selects one
+ * item, CTRL+left click selects multiple items and SHIFT-left click selects
+ * a range of items.
+ */
+ private static final int MULTISELECT_MODE_DEFAULT = 0;
+
+ /**
+ * The simple multiselect mode is what the table used to have before
+ * ctrl/shift selections were added. That is that when this is set clicking
+ * on an item selects/deselects the item and no ctrl/shift selections are
+ * available.
+ */
+ private static final int MULTISELECT_MODE_SIMPLE = 1;
+
+ /**
+ * multiple of pagelength which component will cache when requesting more
+ * rows
+ */
+ private double cache_rate = CACHE_RATE_DEFAULT;
+ /**
+ * fraction of pageLenght which can be scrolled without making new request
+ */
+ private double cache_react_rate = 0.75 * cache_rate;
+
+ public static final char ALIGN_CENTER = 'c';
+ public static final char ALIGN_LEFT = 'b';
+ public static final char ALIGN_RIGHT = 'e';
+ private static final int CHARCODE_SPACE = 32;
+ private int firstRowInViewPort = 0;
+ private int pageLength = 15;
+ private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
+
+ protected boolean showRowHeaders = false;
+
+ private String[] columnOrder;
+
+ protected ApplicationConnection client;
+ protected String paintableId;
+
+ boolean immediate;
+ private boolean nullSelectionAllowed = true;
+
+ private SelectMode selectMode = SelectMode.NONE;
+
+ private final HashSet<String> selectedRowKeys = new HashSet<String>();
+
+ /*
+ * When scrolling and selecting at the same time, the selections are not in
+ * sync with the server while retrieving new rows (until key is released).
+ */
+ private HashSet<Object> unSyncedselectionsBeforeRowFetch;
+
+ /*
+ * These are used when jumping between pages when pressing Home and End
+ */
+ boolean selectLastItemInNextRender = false;
+ boolean selectFirstItemInNextRender = false;
+ boolean focusFirstItemInNextRender = false;
+ boolean focusLastItemInNextRender = false;
+
+ /*
+ * The currently focused row
+ */
+ VScrollTableRow focusedRow;
+
+ /*
+ * Helper to store selection range start in when using the keyboard
+ */
+ VScrollTableRow selectionRangeStart;
+
+ /*
+ * Flag for notifying when the selection has changed and should be sent to
+ * the server
+ */
+ boolean selectionChanged = false;
+
+ /*
+ * The speed (in pixels) which the scrolling scrolls vertically/horizontally
+ */
+ private int scrollingVelocity = 10;
+
+ private Timer scrollingVelocityTimer = null;
+
+ String[] bodyActionKeys;
+
+ private boolean enableDebug = false;
+
+ private static final boolean hasNativeTouchScrolling = BrowserInfo.get()
+ .isTouchDevice()
+ && !BrowserInfo.get().requiresTouchScrollDelegate();
+
+ private Set<String> noncollapsibleColumns;
+
+ /**
+ * The last known row height used to preserve the height of a table with
+ * custom row heights and a fixed page length after removing the last row
+ * from the table.
+ *
+ * A new VScrollTableBody instance is created every time the number of rows
+ * changes causing {@link VScrollTableBody#rowHeight} to be discarded and
+ * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)}
+ * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but
+ * round(3 * 19.8) / 3 = 19.66.
+ */
+ private double lastKnownRowHeight = Double.NaN;
+
+ /**
+ * Represents a select range of rows
+ */
+ private class SelectionRange {
+ private VScrollTableRow startRow;
+ private final int length;
+
+ /**
+ * Constuctor.
+ */
+ public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) {
+ VScrollTableRow endRow;
+ if (row2.isBefore(row1)) {
+ startRow = row2;
+ endRow = row1;
+ } else {
+ startRow = row1;
+ endRow = row2;
+ }
+ length = endRow.getIndex() - startRow.getIndex() + 1;
+ }
+
+ public SelectionRange(VScrollTableRow row, int length) {
+ startRow = row;
+ this.length = length;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+
+ @Override
+ public String toString() {
+ return startRow.getKey() + "-" + length;
+ }
+
+ private boolean inRange(VScrollTableRow row) {
+ return row.getIndex() >= startRow.getIndex()
+ && row.getIndex() < startRow.getIndex() + length;
+ }
+
+ public Collection<SelectionRange> split(VScrollTableRow row) {
+ assert row.isAttached();
+ ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2);
+
+ int endOfFirstRange = row.getIndex() - 1;
+ if (!(endOfFirstRange - startRow.getIndex() < 0)) {
+ // create range of first part unless its length is < 1
+ ranges.add(new SelectionRange(startRow, endOfFirstRange
+ - startRow.getIndex() + 1));
+ }
+ int startOfSecondRange = row.getIndex() + 1;
+ if (!(getEndIndex() - startOfSecondRange < 0)) {
+ // create range of second part unless its length is < 1
+ VScrollTableRow startOfRange = scrollBody
+ .getRowByRowIndex(startOfSecondRange);
+ ranges.add(new SelectionRange(startOfRange, getEndIndex()
+ - startOfSecondRange + 1));
+ }
+ return ranges;
+ }
+
+ private int getEndIndex() {
+ return startRow.getIndex() + length - 1;
+ }
+
+ };
+
+ private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>();
+
+ boolean initializedAndAttached = false;
+
+ /**
+ * Flag to indicate if a column width recalculation is needed due update.
+ */
+ boolean headerChangedDuringUpdate = false;
+
+ protected final TableHead tHead = new TableHead();
+
+ final TableFooter tFoot = new TableFooter();
+
+ final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(true);
+
+ private KeyPressHandler navKeyPressHandler = new KeyPressHandler() {
+
+ @Override
+ public void onKeyPress(KeyPressEvent keyPressEvent) {
+ // This is used for Firefox only, since Firefox auto-repeat
+ // works correctly only if we use a key press handler, other
+ // browsers handle it correctly when using a key down handler
+ if (!BrowserInfo.get().isGecko()) {
+ return;
+ }
+
+ NativeEvent event = keyPressEvent.getNativeEvent();
+ if (!enabled) {
+ // Cancel default keyboard events on a disabled Table
+ // (prevents scrolling)
+ event.preventDefault();
+ } else if (hasFocus) {
+ // Key code in Firefox/onKeyPress is present only for
+ // special keys, otherwise 0 is returned
+ int keyCode = event.getKeyCode();
+ if (keyCode == 0 && event.getCharCode() == ' ') {
+ // Provide a keyCode for space to be compatible with
+ // FireFox keypress event
+ keyCode = CHARCODE_SPACE;
+ }
+
+ if (handleNavigation(keyCode,
+ event.getCtrlKey() || event.getMetaKey(),
+ event.getShiftKey())) {
+ event.preventDefault();
+ }
+
+ startScrollingVelocityTimer();
+ }
+ }
+
+ };
+
+ private KeyUpHandler navKeyUpHandler = new KeyUpHandler() {
+
+ @Override
+ public void onKeyUp(KeyUpEvent keyUpEvent) {
+ NativeEvent event = keyUpEvent.getNativeEvent();
+ int keyCode = event.getKeyCode();
+
+ if (!isFocusable()) {
+ cancelScrollingVelocityTimer();
+ } else if (isNavigationKey(keyCode)) {
+ if (keyCode == getNavigationDownKey()
+ || keyCode == getNavigationUpKey()) {
+ /*
+ * in multiselect mode the server may still have value from
+ * previous page. Clear it unless doing multiselection or
+ * just moving focus.
+ */
+ if (!event.getShiftKey() && !event.getCtrlKey()) {
+ instructServerToForgetPreviousSelections();
+ }
+ sendSelectedRows();
+ }
+ cancelScrollingVelocityTimer();
+ navKeyDown = false;
+ }
+ }
+ };
+
+ private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {
+
+ @Override
+ public void onKeyDown(KeyDownEvent keyDownEvent) {
+ NativeEvent event = keyDownEvent.getNativeEvent();
+ // This is not used for Firefox
+ if (BrowserInfo.get().isGecko()) {
+ return;
+ }
+
+ if (!enabled) {
+ // Cancel default keyboard events on a disabled Table
+ // (prevents scrolling)
+ event.preventDefault();
+ } else if (hasFocus) {
+ if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
+ || event.getMetaKey(), event.getShiftKey())) {
+ navKeyDown = true;
+ event.preventDefault();
+ }
+
+ startScrollingVelocityTimer();
+ }
+ }
+ };
+ int totalRows;
+
+ private Set<String> collapsedColumns;
+
+ final RowRequestHandler rowRequestHandler;
+ VScrollTableBody scrollBody;
+ private int firstvisible = 0;
+ private boolean sortAscending;
+ private String sortColumn;
+ private String oldSortColumn;
+ 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;
+ boolean enabled;
+ boolean showColHeaders;
+ boolean showColFooters;
+
+ /** flag to indicate that table body has changed */
+ private boolean isNewBody = true;
+
+ /*
+ * 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;
+
+ boolean rendering = false;
+ private boolean hasFocus = false;
+ private int dragmode;
+
+ private int multiselectmode;
+ int tabIndex;
+ private TouchScrollDelegate touchScrollDelegate;
+
+ int lastRenderedHeight;
+
+ /**
+ * Values (serverCacheFirst+serverCacheLast) sent by server that tells which
+ * rows (indexes) are in the server side cache (page buffer). -1 means
+ * unknown. The server side cache row MUST MATCH the client side cache rows.
+ *
+ * If the client side cache contains additional rows with e.g. buttons, it
+ * will cause out of sync when such a button is pressed.
+ *
+ * If the server side cache contains additional rows with e.g. buttons,
+ * scrolling in the client will cause empty buttons to be rendered
+ * (cached=true request for non-existing components)
+ */
+ int serverCacheFirst = -1;
+ int serverCacheLast = -1;
+
+ boolean sizeNeedsInit = true;
+
+ /**
+ * Used to recall the position of an open context menu if we need to close
+ * and reopen it during a row update.
+ */
+ class ContextMenuDetails {
+ String rowKey;
+ int left;
+ int top;
+
+ ContextMenuDetails(String rowKey, int left, int top) {
+ this.rowKey = rowKey;
+ this.left = left;
+ this.top = top;
+ }
+ }
+
+ protected ContextMenuDetails contextMenu = null;
+
+ public VScrollTable() {
+ setMultiSelectMode(MULTISELECT_MODE_DEFAULT);
+
+ scrollBodyPanel.addStyleName(CLASSNAME + "-body-wrapper");
+ scrollBodyPanel.addFocusHandler(this);
+ scrollBodyPanel.addBlurHandler(this);
+
+ scrollBodyPanel.addScrollHandler(this);
+ scrollBodyPanel.addStyleName(CLASSNAME + "-body");
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
+ } else {
+ scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
+ }
+ scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);
+
+ scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS);
+
+ scrollBodyPanel.sinkEvents(Event.ONCONTEXTMENU);
+ scrollBodyPanel.addDomHandler(new ContextMenuHandler() {
+
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ handleBodyContextMenu(event);
+ }
+ }, ContextMenuEvent.getType());
+
+ setStyleName(CLASSNAME);
+
+ add(tHead);
+ add(scrollBodyPanel);
+ add(tFoot);
+
+ rowRequestHandler = new RowRequestHandler();
+ }
+
+ public void init(ApplicationConnection client) {
+ this.client = client;
+ // Add a handler to clear saved context menu details when the menu
+ // closes. See #8526.
+ client.getContextMenu().addCloseHandler(new CloseHandler<PopupPanel>() {
+
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ contextMenu = null;
+ }
+ });
+ }
+
+ private void handleBodyContextMenu(ContextMenuEvent event) {
+ if (enabled && bodyActionKeys != null) {
+ int left = Util.getTouchOrMouseClientX(event.getNativeEvent());
+ int top = Util.getTouchOrMouseClientY(event.getNativeEvent());
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+
+ // Only prevent browser context menu if there are action handlers
+ // registered
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Fires a column resize event which sends the resize information to the
+ * server.
+ *
+ * @param columnId
+ * The columnId of the column which was resized
+ * @param originalWidth
+ * The width in pixels of the column before the resize event
+ * @param newWidth
+ * The width in pixels of the column after the resize event
+ */
+ private void fireColumnResizeEvent(String columnId, int originalWidth,
+ int newWidth) {
+ client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
+ false);
+ client.updateVariable(paintableId, "columnResizeEventPrev",
+ originalWidth, false);
+ client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
+ immediate);
+
+ }
+
+ /**
+ * Non-immediate variable update of column widths for a collection of
+ * columns.
+ *
+ * @param columns
+ * the columns to trigger the events for.
+ */
+ private void sendColumnWidthUpdates(Collection<HeaderCell> columns) {
+ String[] newSizes = new String[columns.size()];
+ int ix = 0;
+ for (HeaderCell cell : columns) {
+ newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
+ }
+ client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
+ false);
+ }
+
+ /**
+ * Moves the focus one step down
+ *
+ * @return Returns true if succeeded
+ */
+ private boolean moveFocusDown() {
+ return moveFocusDown(0);
+ }
+
+ /**
+ * Moves the focus down by 1+offset rows
+ *
+ * @return Returns true if succeeded, else false if the selection could not
+ * be move downwards
+ */
+ private boolean moveFocusDown(int offset) {
+ if (isSelectable()) {
+ if (focusedRow == null && scrollBody.iterator().hasNext()) {
+ // FIXME should focus first visible from top, not first rendered
+ // ??
+ return setRowFocus((VScrollTableRow) scrollBody.iterator()
+ .next());
+ } else {
+ VScrollTableRow next = getNextRow(focusedRow, offset);
+ if (next != null) {
+ return setRowFocus(next);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Moves the selection one step up
+ *
+ * @return Returns true if succeeded
+ */
+ private boolean moveFocusUp() {
+ return moveFocusUp(0);
+ }
+
+ /**
+ * Moves the focus row upwards
+ *
+ * @return Returns true if succeeded, else false if the selection could not
+ * be move upwards
+ *
+ */
+ private boolean moveFocusUp(int offset) {
+ if (isSelectable()) {
+ if (focusedRow == null && scrollBody.iterator().hasNext()) {
+ // FIXME logic is exactly the same as in moveFocusDown, should
+ // be the opposite??
+ return setRowFocus((VScrollTableRow) scrollBody.iterator()
+ .next());
+ } else {
+ VScrollTableRow prev = getPreviousRow(focusedRow, offset);
+ if (prev != null) {
+ return setRowFocus(prev);
+ } else {
+ VConsole.log("no previous available");
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Selects a row where the current selection head is
+ *
+ * @param ctrlSelect
+ * Is the selection a ctrl+selection
+ * @param shiftSelect
+ * Is the selection a shift+selection
+ * @return Returns truw
+ */
+ private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
+ if (focusedRow != null) {
+ // Arrows moves the selection and clears previous selections
+ if (isSelectable() && !ctrlSelect && !shiftSelect) {
+ deselectAll();
+ focusedRow.toggleSelection();
+ selectionRangeStart = focusedRow;
+ } else if (isSelectable() && ctrlSelect && !shiftSelect) {
+ // Ctrl+arrows moves selection head
+ selectionRangeStart = focusedRow;
+ // No selection, only selection head is moved
+ } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
+ // Shift+arrows selection selects a range
+ focusedRow.toggleShiftSelection(shiftSelect);
+ }
+ }
+ }
+
+ /**
+ * Sends the selection to the server if changed since the last update/visit.
+ */
+ protected void sendSelectedRows() {
+ sendSelectedRows(immediate);
+ }
+
+ /**
+ * Sends the selection to the server if it has been changed since the last
+ * update/visit.
+ *
+ * @param immediately
+ * set to true to immediately send the rows
+ */
+ protected void sendSelectedRows(boolean immediately) {
+ // Don't send anything if selection has not changed
+ if (!selectionChanged) {
+ return;
+ }
+
+ // Reset selection changed flag
+ selectionChanged = false;
+
+ // Note: changing the immediateness of this might require changes to
+ // "clickEvent" immediateness also.
+ if (isMultiSelectModeDefault()) {
+ // Convert ranges to a set of strings
+ Set<String> ranges = new HashSet<String>();
+ for (SelectionRange range : selectedRowRanges) {
+ ranges.add(range.toString());
+ }
+
+ // Send the selected row ranges
+ client.updateVariable(paintableId, "selectedRanges",
+ ranges.toArray(new String[selectedRowRanges.size()]), false);
+
+ // clean selectedRowKeys so that they don't contain excess values
+ for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator
+ .hasNext();) {
+ String key = iterator.next();
+ VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
+ if (renderedRowByKey != null) {
+ for (SelectionRange range : selectedRowRanges) {
+ if (range.inRange(renderedRowByKey)) {
+ iterator.remove();
+ }
+ }
+ } else {
+ // orphaned selected key, must be in a range, ignore
+ iterator.remove();
+ }
+
+ }
+ }
+
+ // Send the selected rows
+ client.updateVariable(paintableId, "selected",
+ selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
+ immediately);
+
+ }
+
+ /**
+ * Get the key that moves the selection head upwards. By default it is the
+ * up arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection head downwards. By default it is the
+ * down arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that scrolls to the left in the table. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that scroll to the right on the table. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the space
+ * bar key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return CHARCODE_SPACE;
+ }
+
+ /**
+ * Get the key the moves the selection one page up in the table. By default
+ * this is the Page Up key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageUpKey() {
+ return KeyCodes.KEY_PAGEUP;
+ }
+
+ /**
+ * Get the key the moves the selection one page down in the table. By
+ * default this is the Page Down key but by overriding this you can change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageDownKey() {
+ return KeyCodes.KEY_PAGEDOWN;
+ }
+
+ /**
+ * Get the key the moves the selection to the beginning of the table. By
+ * default this is the Home key but by overriding this you can change the
+ * key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationStartKey() {
+ return KeyCodes.KEY_HOME;
+ }
+
+ /**
+ * Get the key the moves the selection to the end of the table. By default
+ * this is the End key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationEndKey() {
+ return KeyCodes.KEY_END;
+ }
+
+ void initializeRows(UIDL uidl, UIDL rowData) {
+ if (scrollBody != null) {
+ scrollBody.removeFromParent();
+ }
+ scrollBody = createScrollBody();
+
+ scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
+ uidl.getIntAttribute("rows"));
+ scrollBodyPanel.add(scrollBody);
+
+ // New body starts scrolled to the left, make sure the header and footer
+ // are also scrolled to the left
+ tHead.setHorizontalScrollPosition(0);
+ tFoot.setHorizontalScrollPosition(0);
+
+ initialContentReceived = true;
+ sizeNeedsInit = true;
+ scrollBody.restoreRowVisibility();
+ }
+
+ void updateColumnProperties(UIDL uidl) {
+ updateColumnOrder(uidl);
+
+ updateCollapsedColumns(uidl);
+
+ UIDL vc = uidl.getChildByTagName("visiblecolumns");
+ if (vc != null) {
+ tHead.updateCellsFromUIDL(vc);
+ tFoot.updateCellsFromUIDL(vc);
+ }
+
+ updateHeader(uidl.getStringArrayAttribute("vcolorder"));
+ updateFooter(uidl.getStringArrayAttribute("vcolorder"));
+ if (uidl.hasVariable("noncollapsiblecolumns")) {
+ noncollapsibleColumns = uidl
+ .getStringArrayVariableAsSet("noncollapsiblecolumns");
+ }
+ }
+
+ private void updateCollapsedColumns(UIDL uidl) {
+ if (uidl.hasVariable("collapsedcolumns")) {
+ tHead.setColumnCollapsingAllowed(true);
+ collapsedColumns = uidl
+ .getStringArrayVariableAsSet("collapsedcolumns");
+ } else {
+ tHead.setColumnCollapsingAllowed(false);
+ }
+ }
+
+ private void updateColumnOrder(UIDL uidl) {
+ if (uidl.hasVariable("columnorder")) {
+ columnReordering = true;
+ columnOrder = uidl.getStringArrayVariable("columnorder");
+ } else {
+ columnReordering = false;
+ columnOrder = null;
+ }
+ }
+
+ boolean selectSelectedRows(UIDL uidl) {
+ boolean keyboardSelectionOverRowFetchInProgress = false;
+
+ if (uidl.hasVariable("selected")) {
+ final Set<String> selectedKeys = uidl
+ .getStringArrayVariableAsSet("selected");
+ if (scrollBody != null) {
+ Iterator<Widget> iterator = scrollBody.iterator();
+ while (iterator.hasNext()) {
+ /*
+ * Make the focus reflect to the server side state unless we
+ * are currently selecting multiple rows with keyboard.
+ */
+ VScrollTableRow row = (VScrollTableRow) iterator.next();
+ boolean selected = selectedKeys.contains(row.getKey());
+ if (!selected
+ && unSyncedselectionsBeforeRowFetch != null
+ && unSyncedselectionsBeforeRowFetch.contains(row
+ .getKey())) {
+ selected = true;
+ keyboardSelectionOverRowFetchInProgress = true;
+ }
+ if (selected != row.isSelected()) {
+ row.toggleSelection();
+ if (!isSingleSelectMode() && !selected) {
+ // Update selection range in case a row is
+ // unselected from the middle of a range - #8076
+ removeRowFromUnsentSelectionRanges(row);
+ }
+ }
+ }
+ }
+ }
+ unSyncedselectionsBeforeRowFetch = null;
+ return keyboardSelectionOverRowFetchInProgress;
+ }
+
+ void updateSortingProperties(UIDL uidl) {
+ oldSortColumn = sortColumn;
+ if (uidl.hasVariable("sortascending")) {
+ sortAscending = uidl.getBooleanVariable("sortascending");
+ sortColumn = uidl.getStringVariable("sortcolumn");
+ }
+ }
+
+ void resizeSortedColumnForSortIndicator() {
+ // Force recalculation of the captionContainer element inside the header
+ // cell to accomodate for the size of the sort arrow.
+ HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
+ if (sortedHeader != null) {
+ tHead.resizeCaptionContainer(sortedHeader);
+ }
+ // Also recalculate the width of the captionContainer element in the
+ // previously sorted header, since this now has more room.
+ HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
+ if (oldSortedHeader != null) {
+ tHead.resizeCaptionContainer(oldSortedHeader);
+ }
+ }
+
+ void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
+ firstvisible = uidl.hasVariable("firstvisible") ? uidl
+ .getIntVariable("firstvisible") : 0;
+ if (firstvisible != lastRequestedFirstvisible && scrollBody != null) {
+ // received 'surprising' firstvisible from server: scroll there
+ firstRowInViewPort = firstvisible;
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstvisible));
+ }
+ }
+
+ protected int measureRowHeightOffset(int rowIx) {
+ return (int) (rowIx * scrollBody.getRowHeight());
+ }
+
+ void updatePageLength(UIDL uidl) {
+ int oldPageLength = pageLength;
+ if (uidl.hasAttribute("pagelength")) {
+ pageLength = uidl.getIntAttribute("pagelength");
+ } else {
+ // pagelenght is "0" meaning scrolling is turned off
+ pageLength = totalRows;
+ }
+
+ if (oldPageLength != pageLength && initializedAndAttached) {
+ // page length changed, need to update size
+ sizeNeedsInit = true;
+ }
+ }
+
+ void updateSelectionProperties(UIDL uidl, ComponentState state,
+ boolean readOnly) {
+ setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl
+ .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT);
+
+ nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl
+ .getBooleanAttribute("nsa") : true;
+
+ if (uidl.hasAttribute("selectmode")) {
+ if (readOnly) {
+ selectMode = SelectMode.NONE;
+ } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
+ selectMode = SelectMode.MULTI;
+ } else if (uidl.getStringAttribute("selectmode").equals("single")) {
+ selectMode = SelectMode.SINGLE;
+ } else {
+ selectMode = SelectMode.NONE;
+ }
+ }
+ }
+
+ void updateDragMode(UIDL uidl) {
+ dragmode = uidl.hasAttribute("dragmode") ? uidl
+ .getIntAttribute("dragmode") : 0;
+ if (BrowserInfo.get().isIE()) {
+ if (dragmode > 0) {
+ getElement().setPropertyJSO("onselectstart",
+ getPreventTextSelectionIEHack());
+ } else {
+ getElement().setPropertyJSO("onselectstart", null);
+ }
+ }
+ }
+
+ protected void updateTotalRows(UIDL uidl) {
+ int newTotalRows = uidl.getIntAttribute("totalrows");
+ if (newTotalRows != getTotalRows()) {
+ if (scrollBody != null) {
+ if (getTotalRows() == 0) {
+ tHead.clear();
+ tFoot.clear();
+ }
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ setTotalRows(newTotalRows);
+ }
+ }
+
+ protected void setTotalRows(int newTotalRows) {
+ totalRows = newTotalRows;
+ }
+
+ public int getTotalRows() {
+ return totalRows;
+ }
+
+ void focusRowFromBody() {
+ if (selectedRowKeys.size() == 1) {
+ // try to focus a row currently selected and in viewport
+ String selectedRowKey = selectedRowKeys.iterator().next();
+ if (selectedRowKey != null) {
+ VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey);
+ if (renderedRow == null || !renderedRow.isInViewPort()) {
+ setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
+ } else {
+ setRowFocus(renderedRow);
+ }
+ }
+ } else {
+ // multiselect mode
+ setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
+ }
+ }
+
+ protected VScrollTableBody createScrollBody() {
+ return new VScrollTableBody();
+ }
+
+ /**
+ * Selects the last row visible in the table
+ *
+ * @param focusOnly
+ * Should the focus only be moved to the last row
+ */
+ void selectLastRenderedRowInViewPort(boolean focusOnly) {
+ int index = firstRowInViewPort + getFullyVisibleRowCount();
+ VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index);
+ if (lastRowInViewport == null) {
+ // this should not happen in normal situations (white space at the
+ // end of viewport). Select the last rendered as a fallback.
+ lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody
+ .getLastRendered());
+ if (lastRowInViewport == null) {
+ return; // empty table
+ }
+ }
+ setRowFocus(lastRowInViewport);
+ if (!focusOnly) {
+ selectFocusedRow(false, multiselectPending);
+ sendSelectedRows();
+ }
+ }
+
+ /**
+ * Selects the first row visible in the table
+ *
+ * @param focusOnly
+ * Should the focus only be moved to the first row
+ */
+ void selectFirstRenderedRowInViewPort(boolean focusOnly) {
+ int index = firstRowInViewPort;
+ VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index);
+ if (firstInViewport == null) {
+ // this should not happen in normal situations
+ return;
+ }
+ setRowFocus(firstInViewport);
+ if (!focusOnly) {
+ selectFocusedRow(false, multiselectPending);
+ sendSelectedRows();
+ }
+ }
+
+ void setCacheRateFromUIDL(UIDL uidl) {
+ setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr")
+ : CACHE_RATE_DEFAULT);
+ }
+
+ private void setCacheRate(double d) {
+ if (cache_rate != d) {
+ cache_rate = d;
+ cache_react_rate = 0.75 * d;
+ }
+ }
+
+ void updateActionMap(UIDL mainUidl) {
+ UIDL actionsUidl = mainUidl.getChildByTagName("actions");
+ if (actionsUidl == null) {
+ return;
+ }
+
+ final Iterator<?> it = actionsUidl.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.translateVaadinUri(action
+ .getStringAttribute("icon")));
+ } else {
+ actionMap.remove(key + "_i");
+ }
+ }
+
+ }
+
+ 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(ROW_HEADER_COLUMN_KEY, colIndex);
+ visibleCols++;
+ visibleColOrder = new String[visibleCols];
+ visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY;
+ colIndex++;
+ } else {
+ visibleColOrder = new String[visibleCols];
+ tHead.removeCell(ROW_HEADER_COLUMN_KEY);
+ }
+
+ 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);
+ setContainerHeight();
+
+ }
+
+ /**
+ * Updates footers.
+ * <p>
+ * Update headers whould be called before this method is called!
+ * </p>
+ *
+ * @param strings
+ */
+ private void updateFooter(String[] strings) {
+ if (strings == null) {
+ return;
+ }
+
+ // Add dummy column if row headers are present
+ int colIndex = 0;
+ if (showRowHeaders) {
+ tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
+ colIndex++;
+ } else {
+ tFoot.removeCell(ROW_HEADER_COLUMN_KEY);
+ }
+
+ int i;
+ for (i = 0; i < strings.length; i++) {
+ final String cid = strings[i];
+ tFoot.enableColumn(cid, colIndex);
+ colIndex++;
+ }
+
+ tFoot.setVisible(showColFooters);
+ }
+
+ /**
+ * @param uidl
+ * which contains row data
+ * @param firstRow
+ * first row in data set
+ * @param reqRows
+ * amount of rows in data set
+ */
+ void updateBody(UIDL uidl, int firstRow, int reqRows) {
+ if (uidl == null || reqRows < 1) {
+ // container is empty, remove possibly existing rows
+ if (firstRow <= 0) {
+ while (scrollBody.getLastRendered() > scrollBody.firstRendered) {
+ scrollBody.unlinkRow(false);
+ }
+ scrollBody.unlinkRow(false);
+ }
+ return;
+ }
+
+ scrollBody.renderRows(uidl, firstRow, reqRows);
+
+ discardRowsOutsideCacheWindow();
+ }
+
+ void updateRowsInBody(UIDL partialRowUpdates) {
+ if (partialRowUpdates == null) {
+ return;
+ }
+ int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix");
+ int count = partialRowUpdates.getIntAttribute("numurows");
+ scrollBody.unlinkRows(firstRowIx, count);
+ scrollBody.insertRows(partialRowUpdates, firstRowIx, count);
+ }
+
+ /**
+ * Updates the internal cache by unlinking rows that fall outside of the
+ * caching window.
+ */
+ protected void discardRowsOutsideCacheWindow() {
+ int firstRowToKeep = (int) (firstRowInViewPort - pageLength
+ * cache_rate);
+ int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength
+ * cache_rate);
+ debug("Client side calculated cache rows to keep: " + firstRowToKeep
+ + "-" + lastRowToKeep);
+
+ if (serverCacheFirst != -1) {
+ firstRowToKeep = serverCacheFirst;
+ lastRowToKeep = serverCacheLast;
+ debug("Server cache rows that override: " + serverCacheFirst + "-"
+ + serverCacheLast);
+ if (firstRowToKeep < scrollBody.getFirstRendered()
+ || lastRowToKeep > scrollBody.getLastRendered()) {
+ debug("*** Server wants us to keep " + serverCacheFirst + "-"
+ + serverCacheLast + " but we only have rows "
+ + scrollBody.getFirstRendered() + "-"
+ + scrollBody.getLastRendered() + " rendered!");
+ }
+ }
+ discardRowsOutsideOf(firstRowToKeep, lastRowToKeep);
+
+ scrollBody.fixSpacers();
+
+ scrollBody.restoreRowVisibility();
+ }
+
+ private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) {
+ /*
+ * firstDiscarded and lastDiscarded are only calculated for debug
+ * purposes
+ */
+ int firstDiscarded = -1, lastDiscarded = -1;
+ boolean cont = true;
+ while (cont && scrollBody.getLastRendered() > optimalFirstRow
+ && scrollBody.getFirstRendered() < optimalFirstRow) {
+ if (firstDiscarded == -1) {
+ firstDiscarded = scrollBody.getFirstRendered();
+ }
+
+ // removing row from start
+ cont = scrollBody.unlinkRow(true);
+ }
+ if (firstDiscarded != -1) {
+ lastDiscarded = scrollBody.getFirstRendered() - 1;
+ debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
+ }
+ firstDiscarded = lastDiscarded = -1;
+
+ cont = true;
+ while (cont && scrollBody.getLastRendered() > optimalLastRow) {
+ if (lastDiscarded == -1) {
+ lastDiscarded = scrollBody.getLastRendered();
+ }
+
+ // removing row from the end
+ cont = scrollBody.unlinkRow(false);
+ }
+ if (lastDiscarded != -1) {
+ firstDiscarded = scrollBody.getLastRendered() + 1;
+ debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
+ }
+
+ debug("Now in cache: " + scrollBody.getFirstRendered() + "-"
+ + scrollBody.getLastRendered());
+ }
+
+ /**
+ * Inserts rows in the table body or removes them from the table body based
+ * on the commands in the UIDL.
+ *
+ * @param partialRowAdditions
+ * the UIDL containing row updates.
+ */
+ protected void addAndRemoveRows(UIDL partialRowAdditions) {
+ if (partialRowAdditions == null) {
+ return;
+ }
+ if (partialRowAdditions.hasAttribute("hide")) {
+ scrollBody.unlinkAndReindexRows(
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ scrollBody.ensureCacheFilled();
+ } else {
+ if (partialRowAdditions.hasAttribute("delbelow")) {
+ scrollBody.insertRowsDeleteBelow(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ } else {
+ scrollBody.insertAndReindexRows(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ }
+ }
+
+ discardRowsOutsideCacheWindow();
+ }
+
+ /**
+ * 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 (ROW_HEADER_COLUMN_KEY.equals(colKey)) {
+ return 0;
+ }
+ for (int i = 0; i < visibleColOrder.length; i++) {
+ if (visibleColOrder[i].equals(colKey)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private boolean isMultiSelectModeSimple() {
+ return selectMode == SelectMode.MULTI
+ && multiselectmode == MULTISELECT_MODE_SIMPLE;
+ }
+
+ private boolean isSingleSelectMode() {
+ return selectMode == SelectMode.SINGLE;
+ }
+
+ private boolean isMultiSelectModeAny() {
+ return selectMode == SelectMode.MULTI;
+ }
+
+ private boolean isMultiSelectModeDefault() {
+ return selectMode == SelectMode.MULTI
+ && multiselectmode == MULTISELECT_MODE_DEFAULT;
+ }
+
+ private void setMultiSelectMode(int multiselectmode) {
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Always use the simple mode for touch devices that do not have
+ // shift/ctrl keys
+ this.multiselectmode = MULTISELECT_MODE_SIMPLE;
+ } else {
+ this.multiselectmode = multiselectmode;
+ }
+
+ }
+
+ protected boolean isSelectable() {
+ return selectMode.getId() > SelectMode.NONE.getId();
+ }
+
+ 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 hcell = tHead.getHeaderCell(colIndex);
+
+ // Make sure that the column grows to accommodate the sort indicator if
+ // necessary.
+ if (w < hcell.getMinWidth()) {
+ w = hcell.getMinWidth();
+ }
+
+ // Set header column width
+ hcell.setWidth(w, isDefinedWidth);
+
+ // Ensure indicators have been taken into account
+ tHead.resizeCaptionContainer(hcell);
+
+ // Set body column width
+ scrollBody.setColWidth(colIndex, w);
+
+ // Set footer column width
+ FooterCell fcell = tFoot.getFooterCell(colIndex);
+ fcell.setWidth(w, isDefinedWidth);
+ }
+
+ private int getColWidth(String colKey) {
+ return tHead.getHeaderCell(colKey).getWidth();
+ }
+
+ /**
+ * Get a rendered row by its key
+ *
+ * @param key
+ * The key to search with
+ * @return
+ */
+ public VScrollTableRow getRenderedRowByKey(String key) {
+ if (scrollBody != null) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ VScrollTableRow r = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (r.getKey().equals(key)) {
+ return r;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the next row to the given row
+ *
+ * @param row
+ * The row to calculate from
+ *
+ * @return The next row or null if no row exists
+ */
+ private VScrollTableRow getNextRow(VScrollTableRow row, int offset) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ VScrollTableRow r = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (r == row) {
+ r = null;
+ while (offset >= 0 && it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ offset--;
+ }
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the previous row from the given row
+ *
+ * @param row
+ * The row to calculate from
+ * @return The previous row or null if no row exists
+ */
+ private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) {
+ final Iterator<Widget> it = scrollBody.iterator();
+ final Iterator<Widget> offsetIt = scrollBody.iterator();
+ VScrollTableRow r = null;
+ VScrollTableRow prev = null;
+ while (it.hasNext()) {
+ r = (VScrollTableRow) it.next();
+ if (offset < 0) {
+ prev = (VScrollTableRow) offsetIt.next();
+ }
+ if (r == row) {
+ return prev;
+ }
+ offset--;
+ }
+
+ return null;
+ }
+
+ protected void reOrderColumn(String columnKey, int newIndex) {
+
+ final int oldIndex = getColIndexByKey(columnKey);
+
+ // Change header order
+ tHead.moveCell(oldIndex, newIndex);
+
+ // Change body order
+ scrollBody.moveCol(oldIndex, newIndex);
+
+ // Change footer order
+ tFoot.moveCell(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);
+ if (client.hasEventListeners(this, COLUMN_REORDER_EVENT_ID)) {
+ client.sendPendingVariableChanges();
+ }
+ }
+
+ @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
+ */
+ void sizeInit() {
+ sizeNeedsInit = false;
+
+ scrollBody.setContainerHeight();
+
+ /*
+ * 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();
+ Iterator<Widget> footCells = tFoot.iterator();
+ int i = 0;
+ int totalExplicitColumnsWidths = 0;
+ int total = 0;
+ float expandRatioDivider = 0;
+
+ final int[] widths = new int[tHead.visibleCells.size()];
+
+ tHead.enableBrowserIntelligence();
+ tFoot.enableBrowserIntelligence();
+
+ // first loop: collect natural widths
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ final FooterCell fCell = (FooterCell) footCells.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
+ int headerWidth = hCell.getNaturalColumnWidth(i);
+ int footerWidth = fCell.getNaturalColumnWidth(i);
+ w = headerWidth > footerWidth ? headerWidth : footerWidth;
+ }
+ hCell.setNaturalMinimumColumnWidth(w);
+ fCell.setNaturalMinimumColumnWidth(w);
+ }
+ widths[i] = w;
+ total += w;
+ i++;
+ }
+
+ tHead.disableBrowserIntelligence();
+ tFoot.disableBrowserIntelligence();
+
+ boolean willHaveScrollbarz = willHaveScrollbars();
+
+ // fix "natural" width if width not set
+ if (isDynamicWidth()) {
+ int w = total;
+ w += scrollBody.getCellExtraWidth() * visibleColOrder.length;
+ if (willHaveScrollbarz) {
+ w += Util.getNativeScrollbarSize();
+ }
+ setContentWidth(w);
+ }
+
+ int availW = scrollBody.getAvailableWidth();
+ if (BrowserInfo.get().isIE()) {
+ // Hey IE, are you really sure about this?
+ availW = scrollBody.getAvailableWidth();
+ }
+ availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length;
+
+ if (willHaveScrollbarz) {
+ availW -= Util.getNativeScrollbarSize();
+ }
+
+ // TODO refactor this code to be the same as in resize timer
+ boolean needsReLayout = false;
+
+ if (availW > total) {
+ // natural size is smaller than available space
+ final int extraSpace = availW - total;
+ final int totalWidthR = total - totalExplicitColumnsWidths;
+ int checksum = 0;
+ needsReLayout = true;
+
+ if (extraSpace == 1) {
+ // We cannot divide one single pixel so we give it the first
+ // undefined column
+ headCells = tHead.iterator();
+ i = 0;
+ checksum = availW;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ widths[i]++;
+ break;
+ }
+ i++;
+ }
+
+ } else 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 = Math.round((extraSpace * (hCell
+ .getExpandRatio() / expandRatioDivider)));
+ w += newSpace;
+ widths[i] = w;
+ }
+ checksum += widths[i];
+ i++;
+ }
+ } else if (totalWidthR > 0) {
+ // no expand ratios defined, we will share extra space
+ // relatively to "natural widths" among 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 = Math.round((float) extraSpace
+ * (float) w / totalWidthR);
+ w += newSpace;
+ widths[i] = w;
+ }
+ checksum += widths[i];
+ i++;
+ }
+ }
+
+ if (extraSpace > 0 && checksum != availW) {
+ /*
+ * There might be in some cases a rounding error of 1px when
+ * extra space is divided so if there is one then we give the
+ * first undefined column 1 more pixel
+ */
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ widths[i] += availW - checksum;
+ break;
+ }
+ 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++;
+ }
+
+ initializedAndAttached = true;
+
+ if (needsReLayout) {
+ scrollBody.reLayoutComponents();
+ }
+
+ updatePageLength();
+
+ /*
+ * Fix "natural" height if height is not set. This must be after width
+ * fixing so the components' widths have been adjusted.
+ */
+ if (isDynamicHeight()) {
+ /*
+ * We must force an update of the row height as this point as it
+ * might have been (incorrectly) calculated earlier
+ */
+
+ int bodyHeight;
+ 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 = scrollBody.getOffsetHeight();
+ bodyHeight = scrollBody.getRequiredHeight();
+ } else {
+ bodyHeight = (int) Math.round(scrollBody.getRowHeight(true)
+ * pageLength);
+ }
+ boolean needsSpaceForHorizontalSrollbar = (total > availW);
+ if (needsSpaceForHorizontalSrollbar) {
+ bodyHeight += Util.getNativeScrollbarSize();
+ }
+ scrollBodyPanel.setHeight(bodyHeight + "px");
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+
+ isNewBody = false;
+
+ if (firstvisible > 0) {
+ // Deferred due to some Firefox oddities
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstvisible));
+ firstRowInViewPort = firstvisible;
+ }
+ });
+ }
+
+ if (enabled) {
+ // Do we need cache rows
+ if (scrollBody.getLastRendered() + 1 < firstRowInViewPort
+ + pageLength + (int) cache_react_rate * pageLength) {
+ if (totalRows - 1 > scrollBody.getLastRendered()) {
+ // fetch cache rows
+ int firstInNewSet = scrollBody.getLastRendered() + 1;
+ rowRequestHandler.setReqFirstRow(firstInNewSet);
+ int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate
+ * pageLength);
+ if (lastInNewSet > totalRows - 1) {
+ lastInNewSet = totalRows - 1;
+ }
+ rowRequestHandler.setReqRows(lastInNewSet - firstInNewSet
+ + 1);
+ rowRequestHandler.deferRowFetch(1);
+ }
+ }
+ }
+
+ /*
+ * Ensures the column alignments are correct at initial loading. <br/>
+ * (child components widths are correct)
+ */
+ scrollBody.reLayoutComponents();
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+ });
+ }
+
+ /**
+ * Note, this method is not official api although declared as protected.
+ * Extend at you own risk.
+ *
+ * @return true if content area will have scrollbars visible.
+ */
+ protected boolean willHaveScrollbars() {
+ if (isDynamicHeight()) {
+ if (pageLength < totalRows) {
+ return true;
+ }
+ } else {
+ int fakeheight = (int) Math.round(scrollBody.getRowHeight()
+ * totalRows);
+ int availableHeight = scrollBodyPanel.getElement().getPropertyInt(
+ "clientHeight");
+ if (fakeheight > availableHeight) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void announceScrollPosition() {
+ if (scrollPositionElement == null) {
+ scrollPositionElement = DOM.createDiv();
+ scrollPositionElement.setClassName(CLASSNAME + "-scrollposition");
+ scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE);
+ scrollPositionElement.getStyle().setDisplay(Display.NONE);
+ getElement().appendChild(scrollPositionElement);
+ }
+
+ Style style = scrollPositionElement.getStyle();
+ style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX);
+ style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX);
+
+ // indexes go from 1-totalRows, as rowheaders in index-mode indicate
+ int last = (firstRowInViewPort + pageLength);
+ if (last > totalRows) {
+ last = totalRows;
+ }
+ scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1)
+ + " &ndash; " + (last) + "..." + "</span>");
+ style.setDisplay(Display.BLOCK);
+ }
+
+ void hideScrollPositionAnnotation() {
+ if (scrollPositionElement != null) {
+ DOM.setStyleAttribute(scrollPositionElement, "display", "none");
+ }
+ }
+
+ boolean isScrollPositionVisible() {
+ return scrollPositionElement != null
+ && !scrollPositionElement.getStyle().getDisplay()
+ .equals(Display.NONE.toString());
+ }
+
+ class RowRequestHandler extends Timer {
+
+ private int reqFirstRow = 0;
+ private int reqRows = 0;
+ private boolean isRunning = false;
+
+ public void deferRowFetch() {
+ deferRowFetch(250);
+ }
+
+ public boolean isRunning() {
+ return isRunning;
+ }
+
+ public void deferRowFetch(int msec) {
+ isRunning = true;
+ if (reqRows > 0 && reqFirstRow < totalRows) {
+ schedule(msec);
+
+ // tell scroll position to user if currently "visible" rows are
+ // not rendered
+ if (totalRows > pageLength
+ && ((firstRowInViewPort + pageLength > scrollBody
+ .getLastRendered()) || (firstRowInViewPort < scrollBody
+ .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() || navKeyDown) {
+ // if client connection is busy, don't bother loading it more
+ VConsole.log("Postponed rowfetch");
+ schedule(250);
+ } else {
+
+ int firstToBeRendered = scrollBody.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 = scrollBody.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);
+
+ if (selectionChanged) {
+ unSyncedselectionsBeforeRowFetch = new HashSet<Object>(
+ selectedRowKeys);
+ }
+ isRunning = false;
+ }
+ }
+
+ public int getReqFirstRow() {
+ return reqFirstRow;
+ }
+
+ /**
+ * Sends request to refresh content at this position.
+ */
+ public void refreshContent() {
+ isRunning = true;
+ 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 sortIndicator = 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;
+
+ private boolean sorted;
+
+ public void setSortable(boolean b) {
+ sortable = b;
+ }
+
+ /**
+ * Makes room for the sorting indicator in case the column that the
+ * header cell belongs to is sorted. This is done by resizing the width
+ * of the caption container element by the correct amount
+ */
+ public void resizeCaptionContainer(int rightSpacing) {
+ int captionContainerWidth = width
+ - colResizeWidget.getOffsetWidth() - rightSpacing;
+
+ if (td.getClassName().contains("-asc")
+ || td.getClassName().contains("-desc")) {
+ // Leave room for the sort indicator
+ captionContainerWidth -= sortIndicator.getOffsetWidth();
+ }
+
+ if (captionContainerWidth < 0) {
+ rightSpacing += captionContainerWidth;
+ captionContainerWidth = 0;
+ }
+
+ captionContainer.getStyle().setPropertyPx("width",
+ captionContainerWidth);
+
+ // Apply/Remove spacing if defined
+ if (rightSpacing > 0) {
+ colResizeWidget.getStyle().setMarginLeft(rightSpacing, Unit.PX);
+ } else {
+ colResizeWidget.getStyle().clearMarginLeft();
+ }
+ }
+
+ public void setNaturalMinimumColumnWidth(int w) {
+ naturalWidth = w;
+ }
+
+ public HeaderCell(String colId, String headerText) {
+ cid = colId;
+
+ DOM.setElementProperty(colResizeWidget, "className", CLASSNAME
+ + "-resizer");
+
+ setText(headerText);
+
+ DOM.appendChild(td, colResizeWidget);
+
+ DOM.setElementProperty(sortIndicator, "className", CLASSNAME
+ + "-sort-indicator");
+ DOM.appendChild(td, sortIndicator);
+
+ DOM.setElementProperty(captionContainer, "className", CLASSNAME
+ + "-caption-container");
+
+ // ensure no clipping initially (problem on column additions)
+ DOM.setStyleAttribute(captionContainer, "overflow", "visible");
+
+ DOM.appendChild(td, captionContainer);
+
+ DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU | Event.TOUCHEVENTS);
+
+ setElement(td);
+
+ setAlign(ALIGN_LEFT);
+ }
+
+ public void disableAutoWidthCalculation() {
+ definedWidth = true;
+ expandRatio = 0;
+ }
+
+ public void setWidth(int w, boolean ensureDefinedWidth) {
+ if (ensureDefinedWidth) {
+ definedWidth = true;
+ // on column resize expand ratio becomes zero
+ expandRatio = 0;
+ }
+ 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 {
+ tHead.resizeCaptionContainer(this);
+
+ /*
+ * 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 (scrollBody != null) {
+ int tdWidth = width + scrollBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ int tdWidth = width
+ + scrollBody.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 && width >= 0;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setText(String headerText) {
+ DOM.setInnerHTML(captionContainer, headerText);
+ }
+
+ public String getColKey() {
+ return cid;
+ }
+
+ private void setSorted(boolean sorted) {
+ this.sorted = 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.getEventTarget().cast() == colResizeWidget) {
+ if (dragging
+ && (event.getTypeInt() == Event.ONMOUSEUP || event
+ .getTypeInt() == Event.ONTOUCHEND)) {
+ // Handle releasing column header on spacer #5318
+ handleCaptionEvent(event);
+ } else {
+ onResizeEvent(event);
+ }
+ } else {
+ /*
+ * Ensure focus before handling caption event. Otherwise
+ * variables changed from caption event may be before
+ * variables from other components that fire variables when
+ * they lose focus.
+ */
+ if (event.getTypeInt() == Event.ONMOUSEDOWN
+ || event.getTypeInt() == Event.ONTOUCHSTART) {
+ scrollBodyPanel.setFocus(true);
+ }
+ handleCaptionEvent(event);
+ boolean stopPropagation = true;
+ if (event.getTypeInt() == Event.ONCONTEXTMENU
+ && !client.hasEventListeners(VScrollTable.this,
+ HEADER_CLICK_EVENT_ID)) {
+ // Prevent showing the browser's context menu only when
+ // there is a header click listener.
+ stopPropagation = false;
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+ }
+
+ private void createFloatingCopy() {
+ floatingCopyOfHeaderCell = DOM.createDiv();
+ DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td));
+ floatingCopyOfHeaderCell = DOM
+ .getChild(floatingCopyOfHeaderCell, 2);
+ DOM.setElementProperty(floatingCopyOfHeaderCell, "className",
+ CLASSNAME + "-header-drag");
+ // otherwise might wrap or be cut if narrow column
+ DOM.setStyleAttribute(floatingCopyOfHeaderCell, "width", "auto");
+ 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;
+ }
+
+ /**
+ * Fires a header click event after the user has clicked a column header
+ * cell
+ *
+ * @param event
+ * The click event
+ */
+ private void fireHeaderClickedEvent(Event event) {
+ if (client.hasEventListeners(VScrollTable.this,
+ HEADER_CLICK_EVENT_ID)) {
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+ client.updateVariable(paintableId, "headerClickEvent",
+ details.toString(), false);
+ client.updateVariable(paintableId, "headerClickCID", cid, true);
+ }
+ }
+
+ protected void handleCaptionEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONTOUCHSTART:
+ case Event.ONMOUSEDOWN:
+ if (columnReordering
+ && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (event.getTypeInt() == Event.ONTOUCHSTART) {
+ /*
+ * prevent using this event in e.g. scrolling
+ */
+ event.stopPropagation();
+ }
+ dragging = true;
+ moved = false;
+ colIndex = getColIndexByKey(cid);
+ DOM.setCapture(getElement());
+ headerX = tHead.getAbsoluteLeft();
+ event.preventDefault(); // prevent selecting text &&
+ // generated touch events
+ }
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ if (columnReordering
+ && Util.isTouchEventOrLeftMouseButton(event)) {
+ 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 (Util.isTouchEvent(event)) {
+ /*
+ * Prevent using in e.g. scrolling and prevent generated
+ * events.
+ */
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ if (!moved) {
+ // mouse event was a click to header -> sort column
+ if (sortable && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (sortColumn.equals(cid)) {
+ // just toggle order
+ client.updateVariable(paintableId, "sortascending",
+ !sortAscending, false);
+ } else {
+ // set table sorted by this column
+ client.updateVariable(paintableId, "sortcolumn",
+ cid, false);
+ }
+ // get also cache columns at the same request
+ scrollBodyPanel.setScrollPosition(0);
+ firstvisible = 0;
+ rowRequestHandler.setReqFirstRow(0);
+ rowRequestHandler.setReqRows((int) (2 * pageLength
+ * cache_rate + pageLength));
+ rowRequestHandler.deferRowFetch(); // some validation +
+ // defer 250ms
+ rowRequestHandler.cancel(); // instead of waiting
+ rowRequestHandler.run(); // run immediately
+ }
+ fireHeaderClickedEvent(event);
+ if (Util.isTouchEvent(event)) {
+ /*
+ * Prevent using in e.g. scrolling and prevent generated
+ * events.
+ */
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ break;
+ }
+ break;
+ case Event.ONDBLCLICK:
+ fireHeaderClickedEvent(event);
+ break;
+ case Event.ONTOUCHMOVE:
+ case Event.ONMOUSEMOVE:
+ if (dragging && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (event.getTypeInt() == Event.ONTOUCHMOVE) {
+ /*
+ * prevent using this event in e.g. scrolling
+ */
+ event.stopPropagation();
+ }
+ if (!moved) {
+ createFloatingCopy();
+ moved = true;
+ }
+
+ final int clientX = Util.getTouchOrMouseClientX(event);
+ final int x = clientX + tHead.hTableWrapper.getScrollLeft();
+ 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(clientX, -1);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void onResizeEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+ isResizing = true;
+ DOM.setCapture(getElement());
+ dragStartX = DOM.eventGetClientX(event);
+ colIndex = getColIndexByKey(cid);
+ originalWidth = getWidth();
+ DOM.eventPreventDefault(event);
+ break;
+ case Event.ONMOUSEUP:
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+ isResizing = false;
+ DOM.releaseCapture(getElement());
+ tHead.disableAutoColumnWidthCalculation(this);
+
+ // Ensure last header cell is taking into account possible
+ // column selector
+ HeaderCell lastCell = tHead.getHeaderCell(tHead
+ .getVisibleCellCount() - 1);
+ tHead.resizeCaptionContainer(lastCell);
+ triggerLazyColumnAdjustment(true);
+
+ fireColumnResizeEvent(cid, originalWidth, getColWidth(cid));
+ break;
+ case Event.ONMOUSEMOVE:
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+ if (isResizing) {
+ final int deltaX = DOM.eventGetClientX(event) - dragStartX;
+ if (deltaX == 0) {
+ return;
+ }
+ tHead.disableAutoColumnWidthCalculation(this);
+
+ int newWidth = originalWidth + deltaX;
+ if (newWidth < getMinWidth()) {
+ newWidth = getMinWidth();
+ }
+ setColWidth(colIndex, newWidth, true);
+ triggerLazyColumnAdjustment(false);
+ forceRealignColumnHeaders();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ public int getMinWidth() {
+ int cellExtraWidth = 0;
+ if (scrollBody != null) {
+ cellExtraWidth += scrollBody.getCellExtraWidth();
+ }
+ return cellExtraWidth + sortIndicator.getOffsetWidth();
+ }
+
+ public String getCaption() {
+ return DOM.getInnerText(captionContainer);
+ }
+
+ public boolean isEnabled() {
+ return getParent() != null;
+ }
+
+ public void setAlign(char c) {
+ final String ALIGN_PREFIX = CLASSNAME + "-caption-container-align-";
+ if (align != c) {
+ captionContainer.removeClassName(ALIGN_PREFIX + "center");
+ captionContainer.removeClassName(ALIGN_PREFIX + "right");
+ captionContainer.removeClassName(ALIGN_PREFIX + "left");
+ switch (c) {
+ case ALIGN_CENTER:
+ captionContainer.addClassName(ALIGN_PREFIX + "center");
+ break;
+ case ALIGN_RIGHT:
+ captionContainer.addClassName(ALIGN_PREFIX + "right");
+ break;
+ default:
+ captionContainer.addClassName(ALIGN_PREFIX + "left");
+ 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)
+
+ int hw = captionContainer.getOffsetWidth()
+ + scrollBody.getCellExtraWidth();
+ if (BrowserInfo.get().isGecko()) {
+ hw += sortIndicator.getOffsetWidth();
+ }
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = scrollBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ return naturalWidth;
+ }
+ }
+
+ public void setExpandRatio(float floatAttribute) {
+ if (floatAttribute != expandRatio) {
+ triggerLazyColumnAdjustment(false);
+ }
+ expandRatio = floatAttribute;
+ }
+
+ public float getExpandRatio() {
+ return expandRatio;
+ }
+
+ public boolean isSorted() {
+ return sorted;
+ }
+ }
+
+ /**
+ * HeaderCell that is header cell for row headers.
+ *
+ * Reordering disabled and clicking on it resets sorting.
+ */
+ public class RowHeadersHeaderCell extends HeaderCell {
+
+ RowHeadersHeaderCell() {
+ super(ROW_HEADER_COLUMN_KEY, "");
+ this.setStyleName(CLASSNAME + "-header-cell-rowheader");
+ }
+
+ @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 = 900000;
+
+ ArrayList<Widget> visibleCells = new ArrayList<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(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersHeaderCell());
+ }
+
+ public void resizeCaptionContainer(HeaderCell cell) {
+ HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1);
+
+ // Measure column widths
+ int columnTotalWidth = 0;
+ for (Widget w : visibleCells) {
+ columnTotalWidth += w.getOffsetWidth();
+ }
+
+ if (cell == lastcell
+ && columnSelector.getOffsetWidth() > 0
+ && columnTotalWidth >= div.getOffsetWidth()
+ - columnSelector.getOffsetWidth()
+ && !hasVerticalScrollbar()) {
+ // Ensure column caption is visible when placed under the column
+ // selector widget by shifting and resizing the caption.
+ int offset = 0;
+ int diff = div.getOffsetWidth() - columnTotalWidth;
+ if (diff < columnSelector.getOffsetWidth() && diff > 0) {
+ // If the difference is less than the column selectors width
+ // then just offset by the
+ // difference
+ offset = columnSelector.getOffsetWidth() - diff;
+ } else {
+ // Else offset by the whole column selector
+ offset = columnSelector.getOffsetWidth();
+ }
+ lastcell.resizeCaptionContainer(offset);
+ } else {
+ cell.resizeCaptionContainer(0);
+ }
+ }
+
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersHeaderCell());
+ }
+
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> it = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ boolean refreshContentWidths = false;
+ 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));
+ } else {
+ c.setAlign(ALIGN_LEFT);
+
+ }
+ if (col.hasAttribute("width")) {
+ final String widthStr = col.getStringAttribute("width");
+ // Make sure to accomodate for the sort indicator if
+ // necessary.
+ int width = Integer.parseInt(widthStr);
+ if (width < c.getMinWidth()) {
+ width = c.getMinWidth();
+ }
+ if (width != c.getWidth() && scrollBody != null) {
+ // Do a more thorough update if a column is resized from
+ // the server *after* the header has been properly
+ // initialized
+ final int colIx = getColIndexByKey(c.cid);
+ final int newWidth = width;
+ Scheduler.get().scheduleDeferred(
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ setColWidth(colIx, newWidth, true);
+ }
+ });
+ refreshContentWidths = true;
+ } else {
+ c.setWidth(width, true);
+ }
+ } else if (recalcWidths) {
+ c.setUndefinedWidth();
+ }
+ if (col.hasAttribute("er")) {
+ c.setExpandRatio(col.getFloatAttribute("er"));
+ }
+ if (col.hasAttribute("collapsed")) {
+ // ensure header is properly removed from parent (case when
+ // collapsing happens via servers side api)
+ if (c.isAttached()) {
+ c.removeFromParent();
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ if (refreshContentWidths) {
+ // Recalculate the column sizings if any column has changed
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ triggerLazyColumnAdjustment(true);
+ }
+ });
+ }
+
+ // check for orphaned header cells
+ for (Iterator<String> cit = availableCells.keySet().iterator(); cit
+ .hasNext();) {
+ String cid = cit.next();
+ if (!updated.contains(cid)) {
+ removeCell(cid);
+ cit.remove();
+ // we will need a column width recalculation, since columns
+ // with expand ratios should expand to fill the void.
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ }
+ }
+
+ 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) {
+ hTableWrapper.setScrollLeft(scrollLeft);
+ }
+
+ public void setColumnCollapsingAllowed(boolean cc) {
+ if (cc) {
+ columnSelector.getStyle().setDisplay(Display.BLOCK);
+ } else {
+ columnSelector.getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ public void disableBrowserIntelligence() {
+ hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
+ }
+
+ public void enableBrowserIntelligence() {
+ hTableContainer.getStyle().clearWidth();
+ }
+
+ public void setHeaderCell(int index, HeaderCell cell) {
+ if (cell.isEnabled()) {
+ // we're moving the cell
+ DOM.removeChild(tr, cell.getElement());
+ orphan(cell);
+ visibleCells.remove(cell);
+ }
+ if (index < visibleCells.size()) {
+ // insert to right slot
+ DOM.insertChild(tr, cell.getElement(), index);
+ adopt(cell);
+ visibleCells.add(index, cell);
+ } 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 >= 0 && 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.add(newIndex, hCell);
+ }
+
+ @Override
+ 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.getEventTarget().cast() == columnSelector) {
+ final int left = DOM.getAbsoluteLeft(columnSelector);
+ final int top = DOM.getAbsoluteTop(columnSelector)
+ + DOM.getElementPropertyInt(columnSelector,
+ "offsetHeight");
+ client.getContextMenu().showAt(this, left, top);
+ }
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (client != null) {
+ client.getContextMenu().ensureHidden(this);
+ }
+ }
+
+ class VisibleColumnAction extends Action {
+
+ String colKey;
+ private boolean collapsed;
+ private boolean noncollapsible = false;
+ private VScrollTableRow currentlyFocusedRow;
+
+ public VisibleColumnAction(String colKey) {
+ super(VScrollTable.TableHead.this);
+ this.colKey = colKey;
+ caption = tHead.getHeaderCell(colKey).getCaption();
+ currentlyFocusedRow = focusedRow;
+ }
+
+ @Override
+ public void execute() {
+ if (noncollapsible) {
+ return;
+ }
+ client.getContextMenu().hide();
+ // toggle selected column
+ if (collapsedColumns.contains(colKey)) {
+ collapsedColumns.remove(colKey);
+ } else {
+ tHead.removeCell(colKey);
+ collapsedColumns.add(colKey);
+ triggerLazyColumnAdjustment(true);
+ }
+
+ // update variable to server
+ client.updateVariable(paintableId, "collapsedcolumns",
+ collapsedColumns.toArray(new String[collapsedColumns
+ .size()]), false);
+ // let rowRequestHandler determine proper rows
+ rowRequestHandler.refreshContent();
+ lazyRevertFocusToRow(currentlyFocusedRow);
+ }
+
+ public void setCollapsed(boolean b) {
+ collapsed = b;
+ }
+
+ public void setNoncollapsible(boolean b) {
+ noncollapsible = b;
+ }
+
+ /**
+ * Override default method to distinguish on/off columns
+ */
+
+ @Override
+ public String getHTML() {
+ final StringBuffer buf = new StringBuffer();
+ buf.append("<span class=\"");
+ if (collapsed) {
+ buf.append("v-off");
+ } else {
+ buf.append("v-on");
+ }
+ if (noncollapsible) {
+ buf.append(" v-disabled");
+ }
+ buf.append("\">");
+
+ buf.append(super.getHTML());
+ buf.append("</span>");
+
+ return buf.toString();
+ }
+
+ }
+
+ /*
+ * Returns columns as Action array for column select popup
+ */
+
+ @Override
+ public Action[] getActions() {
+ Object[] cols;
+ if (columnReordering && columnOrder != null) {
+ 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);
+ }
+ if (noncollapsibleColumns.contains(cid)) {
+ a.setNoncollapsible(true);
+ }
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ 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;
+ }
+
+ /**
+ * Disables the automatic calculation of all column widths by forcing
+ * the widths to be "defined" thus turning off expand ratios and such.
+ */
+ public void disableAutoColumnWidthCalculation(HeaderCell source) {
+ for (HeaderCell cell : availableCells.values()) {
+ cell.disableAutoWidthCalculation();
+ }
+ // fire column resize events for all columns but the source of the
+ // resize action, since an event will fire separately for this.
+ ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>(
+ availableCells.values());
+ columns.remove(source);
+ sendColumnWidthUpdates(columns);
+ forceRealignColumnHeaders();
+ }
+ }
+
+ /**
+ * A cell in the footer
+ */
+ public class FooterCell extends Widget {
+ private final Element td = DOM.createTD();
+ private final Element captionContainer = DOM.createDiv();
+ private char align = ALIGN_LEFT;
+ private int width = -1;
+ private float expandRatio = 0;
+ private final String cid;
+ boolean definedWidth = false;
+ private int naturalWidth = -1;
+
+ public FooterCell(String colId, String headerText) {
+ cid = colId;
+
+ setText(headerText);
+
+ DOM.setElementProperty(captionContainer, "className", CLASSNAME
+ + "-footer-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 | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU);
+
+ setElement(td);
+ }
+
+ /**
+ * Sets the text of the footer
+ *
+ * @param footerText
+ * The text in the footer
+ */
+ public void setText(String footerText) {
+ if (footerText == null || footerText.equals("")) {
+ footerText = "&nbsp;";
+ }
+
+ DOM.setInnerHTML(captionContainer, footerText);
+ }
+
+ /**
+ * Set alignment of the text in the cell
+ *
+ * @param c
+ * The alignment which can be ALIGN_CENTER, ALIGN_LEFT,
+ * ALIGN_RIGHT
+ */
+ 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;
+ }
+
+ /**
+ * Get the alignment of the text int the cell
+ *
+ * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT
+ */
+ public char getAlign() {
+ return align;
+ }
+
+ /**
+ * Sets the width of the cell
+ *
+ * @param w
+ * The width of the cell
+ * @param ensureDefinedWidth
+ * Ensures the the given width is not recalculated
+ */
+ 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 {
+ /*
+ * Reduce width with one pixel for the right border since the
+ * footers does not have any spacers between them.
+ */
+ final int borderWidths = 1;
+
+ // Set the container width (check for negative value)
+ captionContainer.getStyle().setPropertyPx("width",
+ Math.max(w - borderWidths, 0));
+
+ /*
+ * 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 (scrollBody != null) {
+ int tdWidth = width + scrollBody.getCellExtraWidth()
+ - borderWidths;
+ setWidth(Math.max(tdWidth, 0) + "px");
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ int tdWidth = width
+ + scrollBody.getCellExtraWidth()
+ - borderWidths;
+ setWidth(Math.max(tdWidth, 0) + "px");
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Sets the width to undefined
+ */
+ public void setUndefinedWidth() {
+ 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 && width >= 0;
+ }
+
+ /**
+ * Returns the pixels width of the footer cell
+ *
+ * @return The width in pixels
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ /**
+ * Sets the expand ratio of the cell
+ *
+ * @param floatAttribute
+ * The expand ratio
+ */
+ public void setExpandRatio(float floatAttribute) {
+ expandRatio = floatAttribute;
+ }
+
+ /**
+ * Returns the expand ration of the cell
+ *
+ * @return The expand ratio
+ */
+ public float getExpandRatio() {
+ return expandRatio;
+ }
+
+ /**
+ * Is the cell enabled?
+ *
+ * @return True if enabled else False
+ */
+ public boolean isEnabled() {
+ return getParent() != null;
+ }
+
+ /**
+ * Handle column clicking
+ */
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled && event != null) {
+ handleCaptionEvent(event);
+
+ if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
+ scrollBodyPanel.setFocus(true);
+ }
+ boolean stopPropagation = true;
+ if (event.getTypeInt() == Event.ONCONTEXTMENU
+ && !client.hasEventListeners(VScrollTable.this,
+ FOOTER_CLICK_EVENT_ID)) {
+ // Show browser context menu if a footer click listener is
+ // not present
+ stopPropagation = false;
+ }
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * Handles a event on the captions
+ *
+ * @param event
+ * The event to handle
+ */
+ protected void handleCaptionEvent(Event event) {
+ if (event.getTypeInt() == Event.ONMOUSEUP
+ || event.getTypeInt() == Event.ONDBLCLICK) {
+ fireFooterClickedEvent(event);
+ }
+ }
+
+ /**
+ * Fires a footer click event after the user has clicked a column footer
+ * cell
+ *
+ * @param event
+ * The click event
+ */
+ private void fireFooterClickedEvent(Event event) {
+ if (client.hasEventListeners(VScrollTable.this,
+ FOOTER_CLICK_EVENT_ID)) {
+ MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+ client.updateVariable(paintableId, "footerClickEvent",
+ details.toString(), false);
+ client.updateVariable(paintableId, "footerClickCID", cid, true);
+ }
+ }
+
+ /**
+ * Returns the column key of the column
+ *
+ * @return The column key
+ */
+ public String getColKey() {
+ return cid;
+ }
+
+ /**
+ * 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() + scrollBody.getCellExtraWidth();
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = scrollBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ return naturalWidth;
+ }
+ }
+
+ public void setNaturalMinimumColumnWidth(int w) {
+ naturalWidth = w;
+ }
+ }
+
+ /**
+ * HeaderCell that is header cell for row headers.
+ *
+ * Reordering disabled and clicking on it resets sorting.
+ */
+ public class RowHeadersFooterCell extends FooterCell {
+
+ RowHeadersFooterCell() {
+ super(ROW_HEADER_COLUMN_KEY, "");
+ }
+
+ @Override
+ protected void handleCaptionEvent(Event event) {
+ // NOP: RowHeaders cannot be reordered
+ // TODO It'd be nice to reset sorting here
+ }
+ }
+
+ /**
+ * The footer of the table which can be seen in the bottom of the Table.
+ */
+ public class TableFooter extends Panel {
+
+ private static final int WRAPPER_WIDTH = 900000;
+
+ ArrayList<Widget> visibleCells = new ArrayList<Widget>();
+ HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>();
+
+ Element div = DOM.createDiv();
+ Element hTableWrapper = DOM.createDiv();
+ Element hTableContainer = DOM.createDiv();
+ Element table = DOM.createTable();
+ Element headerTableBody = DOM.createTBody();
+ Element tr = DOM.createTR();
+
+ public TableFooter() {
+
+ DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden");
+ DOM.setElementProperty(hTableWrapper, "className", CLASSNAME
+ + "-footer");
+
+ DOM.appendChild(table, headerTableBody);
+ DOM.appendChild(headerTableBody, tr);
+ DOM.appendChild(hTableContainer, table);
+ DOM.appendChild(hTableWrapper, hTableContainer);
+ DOM.appendChild(div, hTableWrapper);
+ setElement(div);
+
+ setStyleName(CLASSNAME + "-footer-wrap");
+
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersFooterCell());
+ }
+
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put(ROW_HEADER_COLUMN_KEY,
+ new RowHeadersFooterCell());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client
+ * .ui.Widget)
+ */
+
+ @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;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
+ */
+
+ @Override
+ public Iterator<Widget> iterator() {
+ return visibleCells.iterator();
+ }
+
+ /**
+ * Gets a footer cell which represents the given columnId
+ *
+ * @param cid
+ * The columnId
+ *
+ * @return The cell
+ */
+ public FooterCell getFooterCell(String cid) {
+ return availableCells.get(cid);
+ }
+
+ /**
+ * Gets a footer cell by using a column index
+ *
+ * @param index
+ * The index of the column
+ * @return The Cell
+ */
+ public FooterCell getFooterCell(int index) {
+ if (index < visibleCells.size()) {
+ return (FooterCell) visibleCells.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Updates the cells contents when updateUIDL request is received
+ *
+ * @param uidl
+ * The UIDL
+ */
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> columnIterator = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ while (columnIterator.hasNext()) {
+ final UIDL col = (UIDL) columnIterator.next();
+ final String cid = col.getStringAttribute("cid");
+ updated.add(cid);
+
+ String caption = col.hasAttribute("fcaption") ? col
+ .getStringAttribute("fcaption") : "";
+ FooterCell c = getFooterCell(cid);
+ if (c == null) {
+ c = new FooterCell(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("align")) {
+ c.setAlign(col.getStringAttribute("align").charAt(0));
+ } else {
+ c.setAlign(ALIGN_LEFT);
+
+ }
+ if (col.hasAttribute("width")) {
+ if (scrollBody == null) {
+ // Already updated by setColWidth called from
+ // TableHeads.updateCellsFromUIDL in case of a server
+ // side resize
+ 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"));
+ }
+ if (col.hasAttribute("collapsed")) {
+ // ensure header is properly removed from parent (case when
+ // collapsing happens via servers side api)
+ if (c.isAttached()) {
+ c.removeFromParent();
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ // check for orphaned header cells
+ for (Iterator<String> cit = availableCells.keySet().iterator(); cit
+ .hasNext();) {
+ String cid = cit.next();
+ if (!updated.contains(cid)) {
+ removeCell(cid);
+ cit.remove();
+ }
+ }
+ }
+
+ /**
+ * Set a footer cell for a specified column index
+ *
+ * @param index
+ * The index
+ * @param cell
+ * The footer cell
+ */
+ public void setFooterCell(int index, FooterCell cell) {
+ if (cell.isEnabled()) {
+ // we're moving the cell
+ DOM.removeChild(tr, cell.getElement());
+ orphan(cell);
+ visibleCells.remove(cell);
+ }
+ if (index < visibleCells.size()) {
+ // insert to right slot
+ DOM.insertChild(tr, cell.getElement(), index);
+ adopt(cell);
+ visibleCells.add(index, cell);
+ } 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");
+ }
+ }
+
+ /**
+ * Remove a cell by using the columnId
+ *
+ * @param colKey
+ * The columnId to remove
+ */
+ public void removeCell(String colKey) {
+ final FooterCell c = getFooterCell(colKey);
+ remove(c);
+ }
+
+ /**
+ * Enable a column (Sets the footer cell)
+ *
+ * @param cid
+ * The columnId
+ * @param index
+ * The index of the column
+ */
+ public void enableColumn(String cid, int index) {
+ final FooterCell c = getFooterCell(cid);
+ if (!c.isEnabled() || getFooterCell(index) != c) {
+ setFooterCell(index, c);
+ if (initializedAndAttached) {
+ headerChangedDuringUpdate = true;
+ }
+ }
+ }
+
+ /**
+ * Disable browser measurement of the table width
+ */
+ public void disableBrowserIntelligence() {
+ DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH
+ + "px");
+ }
+
+ /**
+ * Enable browser measurement of the table width
+ */
+ public void enableBrowserIntelligence() {
+ DOM.setStyleAttribute(hTableContainer, "width", "");
+ }
+
+ /**
+ * Set the horizontal position in the cell in the footer. This is done
+ * when a horizontal scrollbar is present.
+ *
+ * @param scrollLeft
+ * The value of the leftScroll
+ */
+ public void setHorizontalScrollPosition(int scrollLeft) {
+ hTableWrapper.setScrollLeft(scrollLeft);
+ }
+
+ /**
+ * Swap cells when the column are dragged
+ *
+ * @param oldIndex
+ * The old index of the cell
+ * @param newIndex
+ * The new index of the cell
+ */
+ public void moveCell(int oldIndex, int newIndex) {
+ final FooterCell hCell = getFooterCell(oldIndex);
+ final Element cell = hCell.getElement();
+
+ visibleCells.remove(oldIndex);
+ DOM.removeChild(tr, cell);
+
+ DOM.insertChild(tr, cell, newIndex);
+ visibleCells.add(newIndex, hCell);
+ }
+ }
+
+ /**
+ * This Panel can only contain VScrollTableRow type of widgets. This
+ * "simulates" very large table, keeping spacers which take room of
+ * unrendered rows.
+ *
+ */
+ public class VScrollTableBody extends Panel {
+
+ public static final int DEFAULT_ROW_HEIGHT = 24;
+
+ private double rowHeight = -1;
+
+ private final LinkedList<Widget> renderedRows = new LinkedList<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;
+
+ protected VScrollTableBody() {
+ constructDOM();
+ setElement(container);
+ }
+
+ public VScrollTableRow getRowByRowIndex(int indexInTable) {
+ int internalIndex = indexInTable - firstRendered;
+ if (internalIndex >= 0 && internalIndex < renderedRows.size()) {
+ return (VScrollTableRow) renderedRows.get(internalIndex);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return the height of scrollable body, subpixels ceiled.
+ */
+ public int getRequiredHeight() {
+ return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight()
+ + Util.getRequiredHeight(table);
+ }
+
+ 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);
+ if (BrowserInfo.get().requiresTouchScrollDelegate()) {
+ NodeList<Node> childNodes = container.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Element item = (Element) childNodes.getItem(i);
+ item.getStyle().setProperty("webkitTransform",
+ "translate3d(0,0,0)");
+ }
+ }
+
+ }
+
+ public int getAvailableWidth() {
+ int availW = scrollBodyPanel.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 VScrollTableRow row = createRow((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 VScrollTableRow row = prepareRow((UIDL) it.next());
+ addRow(row);
+ lastRendered++;
+ }
+ fixSpacers();
+ } else if (firstIndex + rows == firstRendered) {
+ final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
+ int i = rows;
+ while (it.hasNext()) {
+ i--;
+ rowArray[i] = prepareRow((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 VScrollTableRow row = prepareRow((UIDL) it.next());
+ firstRendered = firstIndex;
+ lastRendered = firstIndex - 1;
+ addRow(row);
+ lastRendered++;
+ setContainerHeight();
+ fixSpacers();
+ while (it.hasNext()) {
+ addRow(prepareRow((UIDL) it.next()));
+ lastRendered++;
+ }
+ fixSpacers();
+ }
+
+ // this may be a new set of rows due content change,
+ // ensure we have proper cache rows
+ ensureCacheFilled();
+ }
+
+ /**
+ * Ensure we have the correct set of rows on client side, e.g. if the
+ * content on the server side has changed, or the client scroll position
+ * has changed since the last request.
+ */
+ protected void ensureCacheFilled() {
+ 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 < reactFirstRow || firstRendered > reactLastRow) {
+ /*
+ * #8040 - scroll position is completely changed since the
+ * latest request, so request a new set of rows.
+ *
+ * TODO: We should probably check whether the fetched rows match
+ * the current scroll position right when they arrive, so as to
+ * not waste time rendering a set of rows that will never be
+ * visible...
+ */
+ rowRequestHandler.setReqFirstRow(reactFirstRow);
+ rowRequestHandler.setReqRows(reactLastRow - reactFirstRow + 1);
+ rowRequestHandler.deferRowFetch(1);
+ } else if (lastRendered < reactLastRow) {
+ // get some cache rows below visible area
+ rowRequestHandler.setReqFirstRow(lastRendered + 1);
+ rowRequestHandler.setReqRows(reactLastRow - lastRendered);
+ rowRequestHandler.deferRowFetch(1);
+ } else if (firstRendered > 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 received and rendered. So in
+ * some rare situations the table may make two cache visits to
+ * server.
+ */
+ rowRequestHandler.setReqFirstRow(reactFirstRow);
+ rowRequestHandler.setReqRows(firstRendered - reactFirstRow);
+ rowRequestHandler.deferRowFetch(1);
+ }
+ }
+
+ /**
+ * Inserts rows as provided in the rowData starting at firstIndex.
+ *
+ * @param rowData
+ * @param firstIndex
+ * @param rows
+ * the number of rows
+ * @return a list of the rows added.
+ */
+ protected List<VScrollTableRow> insertRows(UIDL rowData,
+ int firstIndex, int rows) {
+ aligns = tHead.getColumnAlignments();
+ final Iterator<?> it = rowData.getChildIterator();
+ List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>();
+
+ if (firstIndex == lastRendered + 1) {
+ while (it.hasNext()) {
+ final VScrollTableRow row = prepareRow((UIDL) it.next());
+ addRow(row);
+ insertedRows.add(row);
+ lastRendered++;
+ }
+ fixSpacers();
+ } else if (firstIndex + rows == firstRendered) {
+ final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
+ int i = rows;
+ while (it.hasNext()) {
+ i--;
+ rowArray[i] = prepareRow((UIDL) it.next());
+ }
+ for (i = 0; i < rows; i++) {
+ addRowBeforeFirstRendered(rowArray[i]);
+ insertedRows.add(rowArray[i]);
+ firstRendered--;
+ }
+ } else {
+ // insert in the middle
+ int ix = firstIndex;
+ while (it.hasNext()) {
+ VScrollTableRow row = prepareRow((UIDL) it.next());
+ insertRowAt(row, ix);
+ insertedRows.add(row);
+ lastRendered++;
+ ix++;
+ }
+ fixSpacers();
+ }
+ return insertedRows;
+ }
+
+ protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData,
+ int firstIndex, int rows) {
+ List<VScrollTableRow> inserted = insertRows(rowData, firstIndex,
+ rows);
+ int actualIxOfFirstRowAfterInserted = firstIndex + rows
+ - firstRendered;
+ for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows
+ .size(); ix++) {
+ VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
+ r.setIndex(r.getIndex() + rows);
+ }
+ setContainerHeight();
+ return inserted;
+ }
+
+ protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex,
+ int rows) {
+ unlinkAllRowsStartingAt(firstIndex);
+ insertRows(rowData, firstIndex, rows);
+ setContainerHeight();
+ }
+
+ /**
+ * 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 VScrollTableRow prepareRow(UIDL uidl) {
+ final VScrollTableRow row = createRow(uidl, aligns);
+ row.initCellWidths();
+ return row;
+ }
+
+ protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
+ if (uidl.hasAttribute("gen_html")) {
+ // This is a generated row.
+ return new VScrollTableGeneratedRow(uidl, aligns2);
+ }
+ return new VScrollTableRow(uidl, aligns2);
+ }
+
+ private void addRowBeforeFirstRendered(VScrollTableRow row) {
+ row.setIndex(firstRendered - 1);
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ tBodyElement.insertBefore(row.getElement(),
+ tBodyElement.getFirstChild());
+ adopt(row);
+ renderedRows.add(0, row);
+ }
+
+ private void addRow(VScrollTableRow row) {
+ row.setIndex(firstRendered + renderedRows.size());
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ tBodyElement.appendChild(row.getElement());
+ adopt(row);
+ renderedRows.add(row);
+ }
+
+ private void insertRowAt(VScrollTableRow row, int index) {
+ row.setIndex(index);
+ if (row.isSelected()) {
+ row.addStyleName("v-selected");
+ }
+ if (index > 0) {
+ VScrollTableRow sibling = getRowByRowIndex(index - 1);
+ tBodyElement
+ .insertAfter(row.getElement(), sibling.getElement());
+ } else {
+ VScrollTableRow sibling = getRowByRowIndex(index);
+ tBodyElement.insertBefore(row.getElement(),
+ sibling.getElement());
+ }
+ adopt(row);
+ int actualIx = index - firstRendered;
+ renderedRows.add(actualIx, row);
+ }
+
+ @Override
+ public Iterator<Widget> iterator() {
+ return renderedRows.iterator();
+ }
+
+ /**
+ * @return false if couldn't remove row
+ */
+ protected boolean unlinkRow(boolean fromBeginning) {
+ if (lastRendered - firstRendered < 0) {
+ return false;
+ }
+ int actualIx;
+ if (fromBeginning) {
+ actualIx = 0;
+ firstRendered++;
+ } else {
+ actualIx = renderedRows.size() - 1;
+ lastRendered--;
+ }
+ if (actualIx >= 0) {
+ unlinkRowAtActualIndex(actualIx);
+ fixSpacers();
+ return true;
+ }
+ return false;
+ }
+
+ protected void unlinkRows(int firstIndex, int count) {
+ if (count < 1) {
+ return;
+ }
+ if (firstRendered > firstIndex
+ && firstRendered < firstIndex + count) {
+ firstIndex = firstRendered;
+ }
+ int lastIndex = firstIndex + count - 1;
+ if (lastRendered < lastIndex) {
+ lastIndex = lastRendered;
+ }
+ for (int ix = lastIndex; ix >= firstIndex; ix--) {
+ unlinkRowAtActualIndex(actualIndex(ix));
+ lastRendered--;
+ }
+ fixSpacers();
+ }
+
+ protected void unlinkAndReindexRows(int firstIndex, int count) {
+ unlinkRows(firstIndex, count);
+ int actualFirstIx = firstIndex - firstRendered;
+ for (int ix = actualFirstIx; ix < renderedRows.size(); ix++) {
+ VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
+ r.setIndex(r.getIndex() - count);
+ }
+ setContainerHeight();
+ }
+
+ protected void unlinkAllRowsStartingAt(int index) {
+ if (firstRendered > index) {
+ index = firstRendered;
+ }
+ for (int ix = renderedRows.size() - 1; ix >= index; ix--) {
+ unlinkRowAtActualIndex(actualIndex(ix));
+ lastRendered--;
+ }
+ fixSpacers();
+ }
+
+ private int actualIndex(int index) {
+ return index - firstRendered;
+ }
+
+ private void unlinkRowAtActualIndex(int index) {
+ final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows
+ .get(index);
+ tBodyElement.removeChild(toBeRemoved.getElement());
+ orphan(toBeRemoved);
+ renderedRows.remove(index);
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Fix container blocks height according to totalRows to avoid
+ * "bouncing" when scrolling
+ */
+ private void setContainerHeight() {
+ fixSpacers();
+ DOM.setStyleAttribute(container, "height",
+ measureRowHeightOffset(totalRows) + "px");
+ }
+
+ private void fixSpacers() {
+ int prepx = measureRowHeightOffset(firstRendered);
+ if (prepx < 0) {
+ prepx = 0;
+ }
+ preSpacer.getStyle().setPropertyPx("height", prepx);
+ int postpx = measureRowHeightOffset(totalRows - 1)
+ - measureRowHeightOffset(lastRendered);
+ if (postpx < 0) {
+ postpx = 0;
+ }
+ postSpacer.getStyle().setPropertyPx("height", postpx);
+ }
+
+ public double getRowHeight() {
+ return getRowHeight(false);
+ }
+
+ public double getRowHeight(boolean forceUpdate) {
+ if (tBodyMeasurementsDone && !forceUpdate) {
+ return rowHeight;
+ } else {
+ if (tBodyElement.getRows().getLength() > 0) {
+ int tableHeight = getTableHeight();
+ int rowCount = tBodyElement.getRows().getLength();
+ rowHeight = tableHeight / (double) rowCount;
+ } else {
+ // Special cases if we can't just measure the current rows
+ if (!Double.isNaN(lastKnownRowHeight)) {
+ // Use previous value if available
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE needs to reflow the table element at this
+ * point to work correctly (e.g.
+ * com.vaadin.tests.components.table.
+ * ContainerSizeChange) - the other code paths
+ * already trigger reflows, but here it must be done
+ * explicitly.
+ */
+ getTableHeight();
+ }
+ rowHeight = lastKnownRowHeight;
+ } else if (isAttached()) {
+ // measure row height by adding a dummy row
+ VScrollTableRow scrollTableRow = new VScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ getRowHeight(forceUpdate);
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ // TODO investigate if this can never happen anymore
+ return DEFAULT_ROW_HEIGHT;
+ }
+ }
+ lastKnownRowHeight = rowHeight;
+ 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) {
+ if (renderedRows.isEmpty()) {
+ // no rows yet rendered
+ return 0;
+ }
+ for (Widget row : renderedRows) {
+ if (!(row instanceof VScrollTableGeneratedRow)) {
+ TableRowElement tr = row.getElement().cast();
+ Element wrapperdiv = tr.getCells().getItem(columnIndex)
+ .getFirstChildElement().cast();
+ return wrapperdiv.getOffsetWidth();
+ }
+ }
+ return 0;
+ } 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) {
+ for (Widget row : renderedRows) {
+ ((VScrollTableRow) row).setCellWidth(colIndex, 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 */
+ VScrollTableRow scrollTableRow = new VScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ detectExtrawidth();
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ boolean noCells = false;
+ TableRowElement item = rows.getItem(0);
+ TableCellElement firstTD = item.getCells().getItem(0);
+ if (firstTD == null) {
+ // content is currently empty, we need to add a fake cell
+ // for measuring
+ noCells = true;
+ VScrollTableRow next = (VScrollTableRow) iterator().next();
+ boolean sorted = tHead.getHeaderCell(0) != null ? tHead
+ .getHeaderCell(0).isSorted() : false;
+ next.addCell(null, "", ALIGN_LEFT, "", true, sorted);
+ firstTD = item.getCells().getItem(0);
+ }
+ com.google.gwt.dom.client.Element wrapper = firstTD
+ .getFirstChildElement();
+ cellExtraWidth = firstTD.getOffsetWidth()
+ - wrapper.getOffsetWidth();
+ if (noCells) {
+ firstTD.getParentElement().removeChild(firstTD);
+ }
+ }
+ }
+
+ private void reLayoutComponents() {
+ for (Widget w : this) {
+ VScrollTableRow r = (VScrollTableRow) 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 VScrollTableRow row = (VScrollTableRow) rows.next();
+
+ final Element td = DOM.getChild(row.getElement(), oldIndex);
+ if (td != null) {
+ DOM.removeChild(row.getElement(), td);
+
+ DOM.insertChild(row.getElement(), td, newIndex);
+ }
+ }
+
+ }
+
+ /**
+ * Restore row visibility which is set to "none" when the row is
+ * rendered (due a performance optimization).
+ */
+ private void restoreRowVisibility() {
+ for (Widget row : renderedRows) {
+ row.getElement().getStyle().setProperty("visibility", "");
+ }
+ }
+
+ public class VScrollTableRow extends Panel implements ActionOwner {
+
+ private static final int TOUCHSCROLL_TIMEOUT = 100;
+ private static final int DRAGMODE_MULTIROW = 2;
+ protected ArrayList<Widget> childWidgets = new ArrayList<Widget>();
+ private boolean selected = false;
+ protected final int rowKey;
+
+ private String[] actionKeys = null;
+ private final TableRowElement rowElement;
+ private int index;
+ private Event touchStart;
+ private static final String ROW_CLASSNAME_EVEN = CLASSNAME + "-row";
+ private static final String ROW_CLASSNAME_ODD = CLASSNAME
+ + "-row-odd";
+ private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
+ private Timer contextTouchTimeout;
+ private Timer dragTouchTimeout;
+ private int touchStartY;
+ private int touchStartX;
+ private TooltipInfo tooltipInfo = null;
+ private Map<TableCellElement, TooltipInfo> cellToolTips = new HashMap<TableCellElement, TooltipInfo>();
+ private boolean isDragging = false;
+
+ private VScrollTableRow(int rowKey) {
+ this.rowKey = rowKey;
+ rowElement = Document.get().createTRElement();
+ setElement(rowElement);
+ DOM.sinkEvents(getElement(), Event.MOUSEEVENTS
+ | Event.TOUCHEVENTS | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU | VTooltip.TOOLTIP_EVENTS);
+ }
+
+ public VScrollTableRow(UIDL uidl, char[] aligns) {
+ this(uidl.getIntAttribute("key"));
+
+ /*
+ * Rendering the rows as hidden improves Firefox and Safari
+ * performance drastically.
+ */
+ getElement().getStyle().setProperty("visibility", "hidden");
+
+ String rowStyle = uidl.getStringAttribute("rowstyle");
+ if (rowStyle != null) {
+ addStyleName(CLASSNAME + "-row-" + rowStyle);
+ }
+
+ String rowDescription = uidl.getStringAttribute("rowdescr");
+ if (rowDescription != null && !rowDescription.equals("")) {
+ tooltipInfo = new TooltipInfo(rowDescription);
+ } else {
+ tooltipInfo = null;
+ }
+
+ tHead.getColumnAlignments();
+ int col = 0;
+ int visibleColumnIndex = -1;
+
+ // row header
+ if (showRowHeaders) {
+ boolean sorted = tHead.getHeaderCell(col).isSorted();
+ addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++],
+ "rowheader", true, sorted);
+ visibleColumnIndex++;
+ }
+
+ if (uidl.hasAttribute("al")) {
+ actionKeys = uidl.getStringArrayAttribute("al");
+ }
+
+ addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex);
+
+ if (uidl.hasAttribute("selected") && !isSelected()) {
+ toggleSelection();
+ }
+ }
+
+ public TooltipInfo getTooltipInfo() {
+ return tooltipInfo;
+ }
+
+ /**
+ * Add a dummy row, used for measurements if Table is empty.
+ */
+ public VScrollTableRow() {
+ this(0);
+ addStyleName(CLASSNAME + "-row");
+ addCell(null, "_", 'b', "", true, false);
+ }
+
+ protected void initCellWidths() {
+ final int cells = tHead.getVisibleCellCount();
+ for (int i = 0; i < cells; i++) {
+ int w = VScrollTable.this.getColWidth(getColKeyByIndex(i));
+ if (w < 0) {
+ w = 0;
+ }
+ setCellWidth(i, w);
+ }
+ }
+
+ protected void setCellWidth(int cellIx, int width) {
+ final Element cell = DOM.getChild(getElement(), cellIx);
+ cell.getFirstChildElement().getStyle()
+ .setPropertyPx("width", width);
+ cell.getStyle().setPropertyPx("width", width);
+ }
+
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ 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);
+ }
+
+ String description = null;
+ if (uidl.hasAttribute("descr-" + columnId)) {
+ description = uidl.getStringAttribute("descr-"
+ + columnId);
+ }
+
+ boolean sorted = tHead.getHeaderCell(col).isSorted();
+ if (cell instanceof String) {
+ addCell(uidl, cell.toString(), aligns[col++], style,
+ isRenderHtmlInCells(), sorted, description);
+ } else {
+ final ComponentConnector cellContent = client
+ .getPaintable((UIDL) cell);
+
+ addCell(uidl, cellContent.getWidget(), aligns[col++],
+ style, sorted);
+ }
+ }
+ }
+
+ /**
+ * Overriding this and returning true causes all text cells to be
+ * rendered as HTML.
+ *
+ * @return always returns false in the default implementation
+ */
+ protected boolean isRenderHtmlInCells() {
+ return false;
+ }
+
+ /**
+ * Detects whether row is visible in tables viewport.
+ *
+ * @return
+ */
+ public boolean isInViewPort() {
+ int absoluteTop = getAbsoluteTop();
+ int scrollPosition = scrollBodyPanel.getScrollPosition();
+ if (absoluteTop < scrollPosition) {
+ return false;
+ }
+ int maxVisible = scrollPosition
+ + scrollBodyPanel.getOffsetHeight() - getOffsetHeight();
+ if (absoluteTop > maxVisible) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Makes a check based on indexes whether the row is before the
+ * compared row.
+ *
+ * @param row1
+ * @return true if this rows index is smaller than in the row1
+ */
+ public boolean isBefore(VScrollTableRow row1) {
+ return getIndex() < row1.getIndex();
+ }
+
+ /**
+ * Sets the index of the row in the whole table. Currently used just
+ * to set even/odd classname
+ *
+ * @param indexInWholeTable
+ */
+ private void setIndex(int indexInWholeTable) {
+ index = indexInWholeTable;
+ boolean isOdd = indexInWholeTable % 2 == 0;
+ // Inverted logic to be backwards compatible with earlier 6.4.
+ // It is very strange because rows 1,3,5 are considered "even"
+ // and 2,4,6 "odd".
+ //
+ // First remove any old styles so that both styles aren't
+ // applied when indexes are updated.
+ removeStyleName(ROW_CLASSNAME_ODD);
+ removeStyleName(ROW_CLASSNAME_EVEN);
+ if (!isOdd) {
+ addStyleName(ROW_CLASSNAME_ODD);
+ } else {
+ addStyleName(ROW_CLASSNAME_EVEN);
+ }
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ client.getContextMenu().ensureHidden(this);
+ }
+
+ public String getKey() {
+ return String.valueOf(rowKey);
+ }
+
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted) {
+ addCell(rowUidl, text, align, style, textIsHTML, sorted, null);
+ }
+
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description) {
+ // String only content is optimized by not using Label widget
+ final TableCellElement td = DOM.createTD().cast();
+ initCellWithText(text, align, style, textIsHTML, sorted,
+ description, td);
+ }
+
+ protected void initCellWithText(String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, final TableCellElement td) {
+ final Element container = DOM.createDiv();
+ String className = CLASSNAME + "-cell-content";
+ if (style != null && !style.equals("")) {
+ className += " " + CLASSNAME + "-cell-content-" + style;
+ }
+ if (sorted) {
+ className += " " + CLASSNAME + "-cell-content-sorted";
+ }
+ 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;
+ }
+ }
+
+ if (description != null && !description.equals("")) {
+ TooltipInfo info = new TooltipInfo(description);
+ cellToolTips.put(td, info);
+ } else {
+ cellToolTips.remove(td);
+ }
+
+ td.appendChild(container);
+ getElement().appendChild(td);
+ }
+
+ public void addCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted) {
+ final TableCellElement td = DOM.createTD().cast();
+ initCellWithWidget(w, align, style, sorted, td);
+ }
+
+ protected void initCellWithWidget(Widget w, char align,
+ String style, boolean sorted, final TableCellElement td) {
+ final Element container = DOM.createDiv();
+ String className = CLASSNAME + "-cell-content";
+ if (style != null && !style.equals("")) {
+ className += " " + CLASSNAME + "-cell-content-" + style;
+ }
+ if (sorted) {
+ className += " " + CLASSNAME + "-cell-content-sorted";
+ }
+ 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);
+ }
+
+ @Override
+ 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;
+ }
+ }
+
+ /**
+ * If there are registered click listeners, sends a click event and
+ * returns true. Otherwise, does nothing and returns false.
+ *
+ * @param event
+ * @param targetTdOrTr
+ * @param immediate
+ * Whether the event is sent immediately
+ * @return Whether a click event was sent
+ */
+ private boolean handleClickEvent(Event event, Element targetTdOrTr,
+ boolean immediate) {
+ if (!client.hasEventListeners(VScrollTable.this,
+ ITEM_CLICK_EVENT_ID)) {
+ // Don't send an event if nobody is listening
+ return false;
+ }
+
+ // 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 = MouseEventDetailsBuilder
+ .buildMouseEventDetails(event);
+
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), immediate);
+
+ return true;
+ }
+
+ public TooltipInfo getTooltip(
+ com.google.gwt.dom.client.Element target) {
+
+ TooltipInfo info = null;
+
+ if (target.hasTagName("TD")) {
+
+ TableCellElement td = (TableCellElement) target.cast();
+ info = cellToolTips.get(td);
+ }
+
+ if (info == null) {
+ info = tooltipInfo;
+ }
+
+ return info;
+ }
+
+ /**
+ * Special handler for touch devices that support native scrolling
+ *
+ * @return Whether the event was handled by this method.
+ */
+ private boolean handleTouchEvent(final Event event) {
+
+ boolean touchEventHandled = false;
+
+ if (enabled && hasNativeTouchScrolling) {
+ final Element targetTdOrTr = getEventTargetTdOrTr(event);
+ final int type = event.getTypeInt();
+
+ switch (type) {
+ case Event.ONTOUCHSTART:
+ touchEventHandled = true;
+ touchStart = event;
+ isDragging = false;
+ Touch touch = event.getChangedTouches().get(0);
+ // save position to fields, touches in events are same
+ // instance during the operation.
+ touchStartX = touch.getClientX();
+ touchStartY = touch.getClientY();
+
+ if (dragmode != 0) {
+ if (dragTouchTimeout == null) {
+ dragTouchTimeout = new Timer() {
+
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ // Start a drag if a finger is held
+ // in place long enough, then moved
+ isDragging = true;
+ }
+ }
+ };
+ }
+ dragTouchTimeout.schedule(TOUCHSCROLL_TIMEOUT);
+ }
+
+ if (actionKeys != null) {
+ if (contextTouchTimeout == null) {
+ contextTouchTimeout = new Timer() {
+
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ // Open the context menu if finger
+ // is held in place long enough.
+ showContextMenu(touchStart);
+ event.preventDefault();
+ touchStart = null;
+ }
+ }
+ };
+ }
+ contextTouchTimeout
+ .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
+ }
+ break;
+ case Event.ONTOUCHMOVE:
+ touchEventHandled = true;
+ if (isSignificantMove(event)) {
+ if (contextTouchTimeout != null) {
+ // Moved finger before the context menu timer
+ // expired, so let the browser handle this as a
+ // scroll.
+ contextTouchTimeout.cancel();
+ contextTouchTimeout = null;
+ }
+ if (!isDragging && dragTouchTimeout != null) {
+ // Moved finger before the drag timer expired,
+ // so let the browser handle this as a scroll.
+ dragTouchTimeout.cancel();
+ dragTouchTimeout = null;
+ }
+
+ if (dragmode != 0 && touchStart != null
+ && isDragging) {
+ event.preventDefault();
+ event.stopPropagation();
+ startRowDrag(touchStart, type, targetTdOrTr);
+ }
+ touchStart = null;
+ }
+ break;
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ touchEventHandled = true;
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ }
+ if (dragTouchTimeout != null) {
+ dragTouchTimeout.cancel();
+ }
+ if (touchStart != null) {
+ event.preventDefault();
+ event.stopPropagation();
+ if (!BrowserInfo.get().isAndroid()) {
+ Util.simulateClickFromTouchEvent(touchStart,
+ this);
+ }
+ touchStart = null;
+ }
+ isDragging = false;
+ break;
+ }
+ }
+ return touchEventHandled;
+ }
+
+ /*
+ * React on click that occur on content cells only
+ */
+
+ @Override
+ public void onBrowserEvent(final Event event) {
+
+ final boolean touchEventHandled = handleTouchEvent(event);
+
+ if (enabled && !touchEventHandled) {
+ final int type = event.getTypeInt();
+ final Element targetTdOrTr = getEventTargetTdOrTr(event);
+ if (type == Event.ONCONTEXTMENU) {
+ showContextMenu(event);
+ if (enabled
+ && (actionKeys != null || client
+ .hasEventListeners(VScrollTable.this,
+ ITEM_CLICK_EVENT_ID))) {
+ /*
+ * Prevent browser context menu only if there are
+ * action handlers or item click listeners
+ * registered
+ */
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ return;
+ }
+
+ boolean targetCellOrRowFound = targetTdOrTr != null;
+
+ switch (type) {
+ case Event.ONDBLCLICK:
+ if (targetCellOrRowFound) {
+ handleClickEvent(event, targetTdOrTr, true);
+ }
+ break;
+ case Event.ONMOUSEUP:
+ if (targetCellOrRowFound) {
+ /*
+ * Queue here, send at the same time as the
+ * corresponding value change event - see #7127
+ */
+ boolean clickEventSent = handleClickEvent(event,
+ targetTdOrTr, false);
+
+ if (event.getButton() == Event.BUTTON_LEFT
+ && isSelectable()) {
+
+ // Ctrl+Shift click
+ if ((event.getCtrlKey() || event.getMetaKey())
+ && event.getShiftKey()
+ && isMultiSelectModeDefault()) {
+ toggleShiftSelection(false);
+ setRowFocus(this);
+
+ // Ctrl click
+ } else if ((event.getCtrlKey() || event
+ .getMetaKey())
+ && isMultiSelectModeDefault()) {
+ boolean wasSelected = isSelected();
+ toggleSelection();
+ setRowFocus(this);
+ /*
+ * next possible range select must start on
+ * this row
+ */
+ selectionRangeStart = this;
+ if (wasSelected) {
+ removeRowFromUnsentSelectionRanges(this);
+ }
+
+ } else if ((event.getCtrlKey() || event
+ .getMetaKey()) && isSingleSelectMode()) {
+ // Ctrl (or meta) click (Single selection)
+ if (!isSelected()
+ || (isSelected() && nullSelectionAllowed)) {
+
+ if (!isSelected()) {
+ deselectAll();
+ }
+
+ toggleSelection();
+ setRowFocus(this);
+ }
+
+ } else if (event.getShiftKey()
+ && isMultiSelectModeDefault()) {
+ // Shift click
+ toggleShiftSelection(true);
+
+ } else {
+ // click
+ boolean currentlyJustThisRowSelected = selectedRowKeys
+ .size() == 1
+ && selectedRowKeys
+ .contains(getKey());
+
+ if (!currentlyJustThisRowSelected) {
+ if (isSingleSelectMode()
+ || isMultiSelectModeDefault()) {
+ /*
+ * For default multi select mode
+ * (ctrl/shift) and for single
+ * select mode we need to clear the
+ * previous selection before
+ * selecting a new one when the user
+ * clicks on a row. Only in
+ * multiselect/simple mode the old
+ * selection should remain after a
+ * normal click.
+ */
+ deselectAll();
+ }
+ toggleSelection();
+ } else if ((isSingleSelectMode() || isMultiSelectModeSimple())
+ && nullSelectionAllowed) {
+ toggleSelection();
+ }/*
+ * else NOP to avoid excessive server
+ * visits (selection is removed with
+ * CTRL/META click)
+ */
+
+ selectionRangeStart = this;
+ setRowFocus(this);
+ }
+
+ // Remove IE text selection hack
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast())
+ .setPropertyJSO("onselectstart",
+ null);
+ }
+ // Queue value change
+ sendSelectedRows(false);
+ }
+ /*
+ * Send queued click and value change events if any
+ * If a click event is sent, send value change with
+ * it regardless of the immediate flag, see #7127
+ */
+ if (immediate || clickEventSent) {
+ client.sendPendingVariableChanges();
+ }
+ }
+ break;
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ if (touchStart != null) {
+ /*
+ * Touch has not been handled as neither context or
+ * drag start, handle it as a click.
+ */
+ Util.simulateClickFromTouchEvent(touchStart, this);
+ touchStart = null;
+ }
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ }
+ break;
+ case Event.ONTOUCHMOVE:
+ if (isSignificantMove(event)) {
+ /*
+ * TODO figure out scroll delegate don't eat events
+ * if row is selected. Null check for active
+ * delegate is as a workaround.
+ */
+ if (dragmode != 0
+ && touchStart != null
+ && (TouchScrollDelegate
+ .getActiveScrollDelegate() == null)) {
+ startRowDrag(touchStart, type, targetTdOrTr);
+ }
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ }
+ /*
+ * Avoid clicks and drags by clearing touch start
+ * flag.
+ */
+ touchStart = null;
+ }
+
+ break;
+ case Event.ONTOUCHSTART:
+ touchStart = event;
+ Touch touch = event.getChangedTouches().get(0);
+ // save position to fields, touches in events are same
+ // isntance during the operation.
+ touchStartX = touch.getClientX();
+ touchStartY = touch.getClientY();
+ /*
+ * Prevent simulated mouse events.
+ */
+ touchStart.preventDefault();
+ if (dragmode != 0 || actionKeys != null) {
+ new Timer() {
+
+ @Override
+ public void run() {
+ TouchScrollDelegate activeScrollDelegate = TouchScrollDelegate
+ .getActiveScrollDelegate();
+ /*
+ * If there's a scroll delegate, check if
+ * we're actually scrolling and handle it.
+ * If no delegate, do nothing here and let
+ * the row handle potential drag'n'drop or
+ * context menu.
+ */
+ if (activeScrollDelegate != null) {
+ if (activeScrollDelegate.isMoved()) {
+ /*
+ * Prevent the row from handling
+ * touch move/end events (the
+ * delegate handles those) and from
+ * doing drag'n'drop or opening a
+ * context menu.
+ */
+ touchStart = null;
+ } else {
+ /*
+ * Scrolling hasn't started, so
+ * cancel delegate and let the row
+ * handle potential drag'n'drop or
+ * context menu.
+ */
+ activeScrollDelegate
+ .stopScrolling();
+ }
+ }
+ }
+ }.schedule(TOUCHSCROLL_TIMEOUT);
+
+ if (contextTouchTimeout == null
+ && actionKeys != null) {
+ contextTouchTimeout = new Timer() {
+
+ @Override
+ public void run() {
+ if (touchStart != null) {
+ showContextMenu(touchStart);
+ touchStart = null;
+ }
+ }
+ };
+ }
+ if (contextTouchTimeout != null) {
+ contextTouchTimeout.cancel();
+ contextTouchTimeout
+ .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
+ }
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ if (targetCellOrRowFound) {
+ setRowFocus(this);
+ ensureFocus();
+ if (dragmode != 0
+ && (event.getButton() == NativeEvent.BUTTON_LEFT)) {
+ startRowDrag(event, type, targetTdOrTr);
+
+ } else if (event.getCtrlKey()
+ || event.getShiftKey()
+ || event.getMetaKey()
+ && isMultiSelectModeDefault()) {
+
+ // Prevent default text selection in Firefox
+ event.preventDefault();
+
+ // Prevent default text selection in IE
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast())
+ .setPropertyJSO(
+ "onselectstart",
+ getPreventTextSelectionIEHack());
+ }
+
+ event.stopPropagation();
+ }
+ }
+ break;
+ case Event.ONMOUSEOUT:
+ break;
+ default:
+ break;
+ }
+ }
+ super.onBrowserEvent(event);
+ }
+
+ private boolean isSignificantMove(Event event) {
+ if (touchStart == null) {
+ // no touch start
+ return false;
+ }
+ /*
+ * TODO calculate based on real distance instead of separate
+ * axis checks
+ */
+ Touch touch = event.getChangedTouches().get(0);
+ if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
+ return true;
+ }
+ return false;
+ }
+
+ protected void startRowDrag(Event event, final int type,
+ Element targetTdOrTr) {
+ VTransferable transferable = new VTransferable();
+ transferable.setDragSource(ConnectorMap.get(client)
+ .getConnector(VScrollTable.this));
+ transferable.setData("itemId", "" + rowKey);
+ NodeList<TableCellElement> cells = rowElement.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ if (cells.getItem(i).isOrHasChild(targetTdOrTr)) {
+ HeaderCell headerCell = tHead.getHeaderCell(i);
+ transferable.setData("propertyId", headerCell.cid);
+ break;
+ }
+ }
+
+ VDragEvent ev = VDragAndDropManager.get().startDrag(
+ transferable, event, true);
+ if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny()
+ && selectedRowKeys.contains("" + rowKey)) {
+ ev.createDragImage(
+ (Element) scrollBody.tBodyElement.cast(), true);
+ Element dragImage = ev.getDragImage();
+ int i = 0;
+ for (Iterator<Widget> iterator = scrollBody.iterator(); iterator
+ .hasNext();) {
+ VScrollTableRow next = (VScrollTableRow) iterator
+ .next();
+ Element child = (Element) dragImage.getChild(i++);
+ if (!selectedRowKeys.contains("" + next.rowKey)) {
+ child.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ }
+ } else {
+ ev.createDragImage(getElement(), true);
+ }
+ if (type == Event.ONMOUSEDOWN) {
+ event.preventDefault();
+ }
+ event.stopPropagation();
+ }
+
+ /**
+ * 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) {
+ final Element eventTarget = event.getEventTarget().cast();
+ Widget widget = Util.findWidget(eventTarget, null);
+ final Element thisTrElement = getElement();
+
+ if (widget != this) {
+ /*
+ * This is a workaround to make Labels, read only TextFields
+ * and Embedded in a Table clickable (see #2688). It is
+ * really not a fix as it does not work with a custom read
+ * only components (not extending VLabel/VEmbedded).
+ */
+ while (widget != null && widget.getParent() != this) {
+ widget = widget.getParent();
+ }
+
+ if (!(widget instanceof VLabel)
+ && !(widget instanceof VEmbedded)
+ && !(widget instanceof VTextField && ((VTextField) widget)
+ .isReadOnly())) {
+ return null;
+ }
+ }
+ if (eventTarget == thisTrElement) {
+ // This was a click on the TR element
+ return thisTrElement;
+ }
+
+ // Iterate upwards until we find the TR element
+ Element element = eventTarget;
+ while (element != null
+ && element.getParentElement().cast() != thisTrElement) {
+ element = element.getParentElement().cast();
+ }
+ return element;
+ }
+
+ public void showContextMenu(Event event) {
+ if (enabled && actionKeys != null) {
+ // Show context menu if there are registered action handlers
+ int left = Util.getTouchOrMouseClientX(event);
+ int top = Util.getTouchOrMouseClientY(event);
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ contextMenu = new ContextMenuDetails(getKey(), left, top);
+ client.getContextMenu().showAt(this, left, top);
+ }
+ }
+
+ /**
+ * Has the row been selected?
+ *
+ * @return Returns true if selected, else false
+ */
+ public boolean isSelected() {
+ return selected;
+ }
+
+ /**
+ * Toggle the selection of the row
+ */
+ public void toggleSelection() {
+ selected = !selected;
+ selectionChanged = true;
+ if (selected) {
+ selectedRowKeys.add(String.valueOf(rowKey));
+ addStyleName("v-selected");
+ } else {
+ removeStyleName("v-selected");
+ selectedRowKeys.remove(String.valueOf(rowKey));
+ }
+ }
+
+ /**
+ * Is called when a user clicks an item when holding SHIFT key down.
+ * This will select a new range from the last focused row
+ *
+ * @param deselectPrevious
+ * Should the previous selected range be deselected
+ */
+ private void toggleShiftSelection(boolean deselectPrevious) {
+
+ /*
+ * Ensures that we are in multiselect mode and that we have a
+ * previous selection which was not a deselection
+ */
+ if (isSingleSelectMode()) {
+ // No previous selection found
+ deselectAll();
+ toggleSelection();
+ return;
+ }
+
+ // Set the selectable range
+ VScrollTableRow endRow = this;
+ VScrollTableRow startRow = selectionRangeStart;
+ if (startRow == null) {
+ startRow = focusedRow;
+ // If start row is null then we have a multipage selection
+ // from
+ // above
+ if (startRow == null) {
+ startRow = (VScrollTableRow) scrollBody.iterator()
+ .next();
+ setRowFocus(endRow);
+ }
+ }
+ // Deselect previous items if so desired
+ if (deselectPrevious) {
+ deselectAll();
+ }
+
+ // we'll ensure GUI state from top down even though selection
+ // was the opposite way
+ if (!startRow.isBefore(endRow)) {
+ VScrollTableRow tmp = startRow;
+ startRow = endRow;
+ endRow = tmp;
+ }
+ SelectionRange range = new SelectionRange(startRow, endRow);
+
+ for (Widget w : scrollBody) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (range.inRange(row)) {
+ if (!row.isSelected()) {
+ row.toggleSelection();
+ }
+ selectedRowKeys.add(row.getKey());
+ }
+ }
+
+ // Add range
+ if (startRow != endRow) {
+ selectedRowRanges.add(range);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ui.IActionOwner#getActions ()
+ */
+
+ @Override
+ 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) {
+
+ @Override
+ public void execute() {
+ super.execute();
+ lazyRevertFocusToRow(VScrollTableRow.this);
+ }
+ };
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ 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 Widget getWidgetForPaintable() {
+ return this;
+ }
+ }
+
+ protected class VScrollTableGeneratedRow extends VScrollTableRow {
+
+ private boolean spanColumns;
+ private boolean htmlContentAllowed;
+
+ public VScrollTableGeneratedRow(UIDL uidl, char[] aligns) {
+ super(uidl, aligns);
+ addStyleName("v-table-generated-row");
+ }
+
+ public boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ @Override
+ protected void initCellWidths() {
+ if (spanColumns) {
+ setSpannedColumnWidthAfterDOMFullyInited();
+ } else {
+ super.initCellWidths();
+ }
+ }
+
+ private void setSpannedColumnWidthAfterDOMFullyInited() {
+ // Defer setting width on spanned columns to make sure that
+ // they are added to the DOM before trying to calculate
+ // widths.
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (showRowHeaders) {
+ setCellWidth(0, tHead.getHeaderCell(0).getWidth());
+ calcAndSetSpanWidthOnCell(1);
+ } else {
+ calcAndSetSpanWidthOnCell(0);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected boolean isRenderHtmlInCells() {
+ return htmlContentAllowed;
+ }
+
+ @Override
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
+ spanColumns = uidl.getBooleanAttribute("gen_span");
+
+ final Iterator<?> cells = uidl.getChildIterator();
+ if (spanColumns) {
+ int colCount = uidl.getChildCount();
+ if (cells.hasNext()) {
+ final Object cell = cells.next();
+ if (cell instanceof String) {
+ addSpannedCell(uidl, cell.toString(), aligns[0],
+ "", htmlContentAllowed, false, null,
+ colCount);
+ } else {
+ addSpannedCell(uidl, (Widget) cell, aligns[0], "",
+ false, colCount);
+ }
+ }
+ } else {
+ super.addCellsFromUIDL(uidl, aligns, col,
+ visibleColumnIndex);
+ }
+ }
+
+ private void addSpannedCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted, int colCount) {
+ TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithWidget(w, align, style, sorted, td);
+ }
+
+ private void addSpannedCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, int colCount) {
+ // String only content is optimized by not using Label widget
+ final TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithText(text, align, style, textIsHTML, sorted,
+ description, td);
+ }
+
+ @Override
+ protected void setCellWidth(int cellIx, int width) {
+ if (isSpanColumns()) {
+ if (showRowHeaders) {
+ if (cellIx == 0) {
+ super.setCellWidth(0, width);
+ } else {
+ // We need to recalculate the spanning TDs width for
+ // every cellIx in order to support column resizing.
+ calcAndSetSpanWidthOnCell(1);
+ }
+ } else {
+ // Same as above.
+ calcAndSetSpanWidthOnCell(0);
+ }
+ } else {
+ super.setCellWidth(cellIx, width);
+ }
+ }
+
+ private void calcAndSetSpanWidthOnCell(final int cellIx) {
+ int spanWidth = 0;
+ for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
+ .getVisibleCellCount(); ix++) {
+ spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
+ }
+ Util.setWidthExcludingPaddingAndBorder((Element) getElement()
+ .getChild(cellIx), spanWidth, 13, false);
+ }
+ }
+
+ /**
+ * Ensure the component has a focus.
+ *
+ * TODO the current implementation simply always calls focus for the
+ * component. In case the Table at some point implements focus/blur
+ * listeners, this method needs to be evolved to conditionally call
+ * focus only if not currently focused.
+ */
+ protected void ensureFocus() {
+ if (!hasFocus) {
+ scrollBodyPanel.setFocus(true);
+ }
+
+ }
+
+ }
+
+ /**
+ * Deselects all items
+ */
+ public void deselectAll() {
+ for (Widget w : scrollBody) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (row.isSelected()) {
+ row.toggleSelection();
+ }
+ }
+ // still ensure all selects are removed from (not necessary rendered)
+ selectedRowKeys.clear();
+ selectedRowRanges.clear();
+ // also notify server that it clears all previous selections (the client
+ // side does not know about the invisible ones)
+ instructServerToForgetPreviousSelections();
+ }
+
+ /**
+ * Used in multiselect mode when the client side knows that all selections
+ * are in the next request.
+ */
+ private void instructServerToForgetPreviousSelections() {
+ client.updateVariable(paintableId, "clearSelections", true, false);
+ }
+
+ /**
+ * Determines the pagelength when the table height is fixed.
+ */
+ public void updatePageLength() {
+ // Only update if visible and enabled
+ if (!isVisible() || !enabled) {
+ return;
+ }
+
+ if (scrollBody == null) {
+ return;
+ }
+
+ if (isDynamicHeight()) {
+ return;
+ }
+
+ int rowHeight = (int) Math.round(scrollBody.getRowHeight());
+ int bodyH = scrollBodyPanel.getOffsetHeight();
+ int rowsAtOnce = bodyH / rowHeight;
+ boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0);
+ if (anotherPartlyVisible) {
+ rowsAtOnce++;
+ }
+ if (pageLength != rowsAtOnce) {
+ pageLength = rowsAtOnce;
+ client.updateVariable(paintableId, "pagelength", pageLength, false);
+
+ if (!rendering) {
+ int currentlyVisible = scrollBody.lastRendered
+ - scrollBody.firstRendered;
+ if (currentlyVisible < pageLength
+ && currentlyVisible < totalRows) {
+ // shake scrollpanel to fill empty space
+ scrollBodyPanel.setScrollPosition(scrollTop + 1);
+ scrollBodyPanel.setScrollPosition(scrollTop - 1);
+ }
+
+ sizeNeedsInit = true;
+ }
+ }
+
+ }
+
+ void updateWidth() {
+ if (!isVisible()) {
+ /*
+ * Do not update size when the table is hidden as all column widths
+ * will be set to zero and they won't be recalculated when the table
+ * is set visible again (until the size changes again)
+ */
+ return;
+ }
+
+ if (!isDynamicWidth()) {
+ int innerPixels = getOffsetWidth() - getBorderWidth();
+ if (innerPixels < 0) {
+ innerPixels = 0;
+ }
+ setContentWidth(innerPixels);
+
+ // readjust undefined width columns
+ triggerLazyColumnAdjustment(false);
+
+ } else {
+
+ sizeNeedsInit = true;
+
+ // readjust undefined width columns
+ triggerLazyColumnAdjustment(false);
+ }
+
+ /*
+ * setting width may affect wheter the component has scrollbars -> needs
+ * scrolling or not
+ */
+ setProperTabIndex();
+ }
+
+ 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() {
+ if (scrollBody == null) {
+ // Try again later if we get here before scrollBody has been
+ // initalized
+ triggerLazyColumnAdjustment(false);
+ return;
+ }
+
+ 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 = scrollBody.getAvailableWidth();
+ // Hey IE, are you really sure about this?
+ availW = scrollBody.getAvailableWidth();
+ int visibleCellCount = tHead.getVisibleCellCount();
+ availW -= scrollBody.getCellExtraWidth() * visibleCellCount;
+ if (willHaveScrollbars()) {
+ availW -= Util.getNativeScrollbarSize();
+ }
+
+ int extraSpace = availW - usedMinimumWidth;
+ if (extraSpace < 0) {
+ extraSpace = 0;
+ }
+
+ int totalUndefinedNaturalWidths = usedMinimumWidth
+ - totalExplicitColumnsWidths;
+
+ // we have some space that can be divided optimally
+ HeaderCell hCell;
+ colIndex = 0;
+ headCells = tHead.iterator();
+ int checksum = 0;
+ 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 = Math.round((w + extraSpace
+ * hCell.getExpandRatio() / expandRatioDivider));
+ } else {
+ if (totalUndefinedNaturalWidths != 0) {
+ // divide relatively to natural column widths
+ newSpace = Math.round(w + (float) extraSpace
+ * (float) w / totalUndefinedNaturalWidths);
+ } else {
+ newSpace = w;
+ }
+ }
+ checksum += newSpace;
+ setColWidth(colIndex, newSpace, false);
+ } else {
+ checksum += hCell.getWidth();
+ }
+ colIndex++;
+ }
+
+ if (extraSpace > 0 && checksum != availW) {
+ /*
+ * There might be in some cases a rounding error of 1px when
+ * extra space is divided so if there is one then we give the
+ * first undefined column 1 more pixel
+ */
+ headCells = tHead.iterator();
+ colIndex = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hc = (HeaderCell) headCells.next();
+ if (!hc.isDefinedWidth()) {
+ setColWidth(colIndex,
+ hc.getWidth() + availW - checksum, false);
+ break;
+ }
+ colIndex++;
+ }
+ }
+
+ if (isDynamicHeight() && totalRows == pageLength) {
+ // fix body height (may vary if lazy loading is offhorizontal
+ // scrollbar appears/disappears)
+ int bodyHeight = scrollBody.getRequiredHeight();
+ boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth);
+ if (needsSpaceForHorizontalScrollbar) {
+ bodyHeight += Util.getNativeScrollbarSize();
+ }
+ int heightBefore = getOffsetHeight();
+ scrollBodyPanel.setHeight(bodyHeight + "px");
+ if (heightBefore != getOffsetHeight()) {
+ Util.notifyParentOfSizeChange(VScrollTable.this, false);
+ }
+ }
+ scrollBody.reLayoutComponents();
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+ });
+
+ forceRealignColumnHeaders();
+ }
+
+ };
+
+ private void forceRealignColumnHeaders() {
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE does not fire onscroll event if scroll position is reverted to
+ * 0 due to the content element size growth. Ensure headers are in
+ * sync with content manually. Safe to use null event as we don't
+ * actually use the event object in listener.
+ */
+ onScroll(null);
+ }
+ }
+
+ /**
+ * helper to set pixel size of head and body part
+ *
+ * @param pixels
+ */
+ private void setContentWidth(int pixels) {
+ tHead.setWidth(pixels + "px");
+ scrollBodyPanel.setWidth(pixels + "px");
+ tFoot.setWidth(pixels + "px");
+ }
+
+ private int borderWidth = -1;
+
+ /**
+ * @return border left + border right
+ */
+ private int getBorderWidth() {
+ if (borderWidth < 0) {
+ borderWidth = Util.measureHorizontalPaddingAndBorder(
+ scrollBodyPanel.getElement(), 2);
+ if (borderWidth < 0) {
+ borderWidth = 0;
+ }
+ }
+ return borderWidth;
+ }
+
+ /**
+ * Ensures scrollable area is properly sized. This method is used when fixed
+ * size is used.
+ */
+ private int containerHeight;
+
+ private void setContainerHeight() {
+ if (!isDynamicHeight()) {
+ containerHeight = getOffsetHeight();
+ containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0;
+ containerHeight -= tFoot.getOffsetHeight();
+ containerHeight -= getContentAreaBorderHeight();
+ if (containerHeight < 0) {
+ containerHeight = 0;
+ }
+ scrollBodyPanel.setHeight(containerHeight + "px");
+ }
+ }
+
+ private int contentAreaBorderHeight = -1;
+ private int scrollLeft;
+ private int scrollTop;
+ VScrollTableDropHandler dropHandler;
+ private boolean navKeyDown;
+ boolean multiselectPending;
+
+ /**
+ * @return border top + border bottom of the scrollable area of table
+ */
+ private int getContentAreaBorderHeight() {
+ if (contentAreaBorderHeight < 0) {
+
+ DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow",
+ "hidden");
+ int oh = scrollBodyPanel.getOffsetHeight();
+ int ch = scrollBodyPanel.getElement()
+ .getPropertyInt("clientHeight");
+ contentAreaBorderHeight = oh - ch;
+ DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow",
+ "auto");
+ }
+ return contentAreaBorderHeight;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (height.length() == 0
+ && getElement().getStyle().getHeight().length() != 0) {
+ /*
+ * Changing from defined to undefined size -> should do a size init
+ * to take page length into account again
+ */
+ sizeNeedsInit = true;
+ }
+ super.setHeight(height);
+ }
+
+ void updateHeight() {
+ setContainerHeight();
+
+ if (initializedAndAttached) {
+ updatePageLength();
+ }
+ if (!rendering) {
+ // Webkit may sometimes get an odd rendering bug (white space
+ // between header and body), see bug #3875. Running
+ // overflow hack here to shake body element a bit.
+ // We must run the fix as a deferred command to prevent it from
+ // overwriting the scroll position with an outdated value, see
+ // #7607.
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ }
+ });
+ }
+
+ triggerLazyColumnAdjustment(false);
+
+ /*
+ * setting height may affect wheter the component has scrollbars ->
+ * needs scrolling or not
+ */
+ setProperTabIndex();
+
+ }
+
+ /*
+ * 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) {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * 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
+ */
+ protected String buildCaptionHtmlSnippet(UIDL uidl) {
+ String s = uidl.hasAttribute("caption") ? uidl
+ .getStringAttribute("caption") : "";
+ if (uidl.hasAttribute("icon")) {
+ s = "<img src=\""
+ + Util.escapeAttribute(client.translateVaadinUri(uidl
+ .getStringAttribute("icon")))
+ + "\" alt=\"icon\" class=\"v-icon\">" + s;
+ }
+ return s;
+ }
+
+ /**
+ * This method has logic which rows needs to be requested from server when
+ * user scrolls
+ */
+
+ @Override
+ public void onScroll(ScrollEvent event) {
+ scrollLeft = scrollBodyPanel.getElement().getScrollLeft();
+ scrollTop = scrollBodyPanel.getScrollPosition();
+ /*
+ * #6970 - IE sometimes fires scroll events for a detached table.
+ *
+ * FIXME initializedAndAttached should probably be renamed - its name
+ * doesn't seem to reflect its semantics. onDetach() doesn't set it to
+ * false, and changing that might break something else, so we need to
+ * check isAttached() separately.
+ */
+ if (!initializedAndAttached || !isAttached()) {
+ return;
+ }
+ if (!enabled) {
+ scrollBodyPanel
+ .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
+ return;
+ }
+
+ rowRequestHandler.cancel();
+
+ if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) {
+ // due to the webkitoverflowworkaround, top may sometimes report 0
+ // for webkit, although it really is not. Expecting to have the
+ // correct
+ // value available soon.
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ onScroll(null);
+ }
+ });
+ return;
+ }
+
+ // fix headers horizontal scrolling
+ tHead.setHorizontalScrollPosition(scrollLeft);
+
+ // fix footers horizontal scrolling
+ tFoot.setHorizontalScrollPosition(scrollLeft);
+
+ firstRowInViewPort = calcFirstRowInViewPort();
+ if (firstRowInViewPort > totalRows - pageLength) {
+ firstRowInViewPort = totalRows - pageLength;
+ }
+
+ int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + 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 = scrollBody.getLastRendered();
+ final int firstRendered = scrollBody.getFirstRendered();
+
+ if (postLimit <= lastRendered && preLimit >= firstRendered) {
+ // we're within no-react area, no need to request more rows
+ // remember which firstvisible we requested, in case the server has
+ // a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ client.updateVariable(paintableId, "firstvisible",
+ firstRowInViewPort, false);
+ return;
+ }
+
+ if (firstRowInViewPort - pageLength * cache_rate > lastRendered
+ || firstRowInViewPort + pageLength + pageLength * cache_rate < firstRendered) {
+ // need a totally new set of rows
+ rowRequestHandler
+ .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
+ int last = firstRowInViewPort + (int) (cache_rate * pageLength)
+ + pageLength - 1;
+ 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();
+ }
+ }
+
+ protected int calcFirstRowInViewPort() {
+ return (int) Math.ceil(scrollTop / scrollBody.getRowHeight());
+ }
+
+ @Override
+ public VScrollTableDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ private static class TableDDDetails {
+ int overkey = -1;
+ VerticalDropLocation dropLocation;
+ String colkey;
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof TableDDDetails) {
+ TableDDDetails other = (TableDDDetails) obj;
+ return dropLocation == other.dropLocation
+ && overkey == other.overkey
+ && ((colkey != null && colkey.equals(other.colkey)) || (colkey == null && other.colkey == null));
+ }
+ return false;
+ }
+
+ //
+ // public int hashCode() {
+ // return overkey;
+ // }
+ }
+
+ public class VScrollTableDropHandler extends VAbstractDropHandler {
+
+ private static final String ROWSTYLEBASE = "v-table-row-drag-";
+ private TableDDDetails dropDetails;
+ private TableDDDetails lastEmphasized;
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ updateDropDetails(drag);
+ super.dragEnter(drag);
+ }
+
+ private void updateDropDetails(VDragEvent drag) {
+ dropDetails = new TableDDDetails();
+ Element elementOver = drag.getElementOver();
+
+ VScrollTableRow row = Util.findWidget(elementOver, getRowClass());
+ if (row != null) {
+ dropDetails.overkey = row.rowKey;
+ Element tr = row.getElement();
+ Element element = elementOver;
+ while (element != null && element.getParentElement() != tr) {
+ element = (Element) element.getParentElement();
+ }
+ int childIndex = DOM.getChildIndex(tr, element);
+ dropDetails.colkey = tHead.getHeaderCell(childIndex)
+ .getColKey();
+ dropDetails.dropLocation = DDUtil.getVerticalDropLocation(
+ row.getElement(), drag.getCurrentGwtEvent(), 0.2);
+ }
+
+ drag.getDropDetails().put("itemIdOver", dropDetails.overkey + "");
+ drag.getDropDetails().put(
+ "detail",
+ dropDetails.dropLocation != null ? dropDetails.dropLocation
+ .toString() : null);
+
+ }
+
+ private Class<? extends Widget> getRowClass() {
+ // get the row type this way to make dd work in derived
+ // implementations
+ return scrollBody.iterator().next().getClass();
+ }
+
+ @Override
+ public void dragOver(VDragEvent drag) {
+ TableDDDetails oldDetails = dropDetails;
+ updateDropDetails(drag);
+ if (!oldDetails.equals(dropDetails)) {
+ deEmphasis();
+ final TableDDDetails newDetails = dropDetails;
+ VAcceptCallback cb = new VAcceptCallback() {
+
+ @Override
+ public void accepted(VDragEvent event) {
+ if (newDetails.equals(dropDetails)) {
+ dragAccepted(event);
+ }
+ /*
+ * Else new target slot already defined, ignore
+ */
+ }
+ };
+ validate(cb, drag);
+ }
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis();
+ super.dragLeave(drag);
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ deEmphasis();
+ return super.drop(drag);
+ }
+
+ private void deEmphasis() {
+ UIObject.setStyleName(getElement(), CLASSNAME + "-drag", false);
+ if (lastEmphasized == null) {
+ return;
+ }
+ for (Widget w : scrollBody.renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (lastEmphasized != null
+ && row.rowKey == lastEmphasized.overkey) {
+ String stylename = ROWSTYLEBASE
+ + lastEmphasized.dropLocation.toString()
+ .toLowerCase();
+ VScrollTableRow.setStyleName(row.getElement(), stylename,
+ false);
+ lastEmphasized = null;
+ return;
+ }
+ }
+ }
+
+ /**
+ * TODO needs different drop modes ?? (on cells, on rows), now only
+ * supports rows
+ */
+ private void emphasis(TableDDDetails details) {
+ deEmphasis();
+ UIObject.setStyleName(getElement(), CLASSNAME + "-drag", true);
+ // iterate old and new emphasized row
+ for (Widget w : scrollBody.renderedRows) {
+ VScrollTableRow row = (VScrollTableRow) w;
+ if (details != null && details.overkey == row.rowKey) {
+ String stylename = ROWSTYLEBASE
+ + details.dropLocation.toString().toLowerCase();
+ VScrollTableRow.setStyleName(row.getElement(), stylename,
+ true);
+ lastEmphasized = details;
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ emphasis(dropDetails);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(VScrollTable.this);
+ }
+
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ }
+
+ protected VScrollTableRow getFocusedRow() {
+ return focusedRow;
+ }
+
+ /**
+ * Moves the selection head to a specific row
+ *
+ * @param row
+ * The row to where the selection head should move
+ * @return Returns true if focus was moved successfully, else false
+ */
+ public boolean setRowFocus(VScrollTableRow row) {
+
+ if (!isSelectable()) {
+ return false;
+ }
+
+ // Remove previous selection
+ if (focusedRow != null && focusedRow != row) {
+ focusedRow.removeStyleName(CLASSNAME_SELECTION_FOCUS);
+ }
+
+ if (row != null) {
+
+ // Apply focus style to new selection
+ row.addStyleName(CLASSNAME_SELECTION_FOCUS);
+
+ /*
+ * Trying to set focus on already focused row
+ */
+ if (row == focusedRow) {
+ return false;
+ }
+
+ // Set new focused row
+ focusedRow = row;
+
+ ensureRowIsVisible(row);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Ensures that the row is visible
+ *
+ * @param row
+ * The row to ensure is visible
+ */
+ private void ensureRowIsVisible(VScrollTableRow row) {
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Skip due to android devices that have broken scrolltop will may
+ // get odd scrolling here.
+ return;
+ }
+ Util.scrollIntoViewVertically(row.getElement());
+ }
+
+ /**
+ * Handles the keyboard events handled by the table
+ *
+ * @param event
+ * The keyboard event received
+ * @return true iff the navigation event was handled
+ */
+ protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+ if (keycode == KeyCodes.KEY_TAB || keycode == KeyCodes.KEY_SHIFT) {
+ // Do not handle tab key
+ return false;
+ }
+
+ // Down navigation
+ if (!isSelectable() && keycode == getNavigationDownKey()) {
+ scrollBodyPanel.setScrollPosition(scrollBodyPanel
+ .getScrollPosition() + scrollingVelocity);
+ return true;
+ } else if (keycode == getNavigationDownKey()) {
+ if (isMultiSelectModeAny() && moveFocusDown()) {
+ selectFocusedRow(ctrl, shift);
+
+ } else if (isSingleSelectMode() && !shift && moveFocusDown()) {
+ selectFocusedRow(ctrl, shift);
+ }
+ return true;
+ }
+
+ // Up navigation
+ if (!isSelectable() && keycode == getNavigationUpKey()) {
+ scrollBodyPanel.setScrollPosition(scrollBodyPanel
+ .getScrollPosition() - scrollingVelocity);
+ return true;
+ } else if (keycode == getNavigationUpKey()) {
+ if (isMultiSelectModeAny() && moveFocusUp()) {
+ selectFocusedRow(ctrl, shift);
+ } else if (isSingleSelectMode() && !shift && moveFocusUp()) {
+ selectFocusedRow(ctrl, shift);
+ }
+ return true;
+ }
+
+ if (keycode == getNavigationLeftKey()) {
+ // Left navigation
+ scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
+ .getHorizontalScrollPosition() - scrollingVelocity);
+ return true;
+
+ } else if (keycode == getNavigationRightKey()) {
+ // Right navigation
+ scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
+ .getHorizontalScrollPosition() + scrollingVelocity);
+ return true;
+ }
+
+ // Select navigation
+ if (isSelectable() && keycode == getNavigationSelectKey()) {
+ if (isSingleSelectMode()) {
+ boolean wasSelected = focusedRow.isSelected();
+ deselectAll();
+ if (!wasSelected || !nullSelectionAllowed) {
+ focusedRow.toggleSelection();
+ }
+ } else {
+ focusedRow.toggleSelection();
+ removeRowFromUnsentSelectionRanges(focusedRow);
+ }
+
+ sendSelectedRows();
+ return true;
+ }
+
+ // Page Down navigation
+ if (keycode == getNavigationPageDownKey()) {
+ if (isSelectable()) {
+ /*
+ * If selectable we plagiate MSW behaviour: first scroll to the
+ * end of current view. If at the end, scroll down one page
+ * length and keep the selected row in the bottom part of
+ * visible area.
+ */
+ if (!isFocusAtTheEndOfTable()) {
+ VScrollTableRow lastVisibleRowInViewPort = scrollBody
+ .getRowByRowIndex(firstRowInViewPort
+ + getFullyVisibleRowCount() - 1);
+ if (lastVisibleRowInViewPort != null
+ && lastVisibleRowInViewPort != focusedRow) {
+ // focused row is not at the end of the table, move
+ // focus and select the last visible row
+ setRowFocus(lastVisibleRowInViewPort);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ } else {
+ int indexOfToBeFocused = focusedRow.getIndex()
+ + getFullyVisibleRowCount();
+ if (indexOfToBeFocused >= totalRows) {
+ indexOfToBeFocused = totalRows - 1;
+ }
+ VScrollTableRow toBeFocusedRow = scrollBody
+ .getRowByRowIndex(indexOfToBeFocused);
+
+ if (toBeFocusedRow != null) {
+ /*
+ * if the next focused row is rendered
+ */
+ setRowFocus(toBeFocusedRow);
+ selectFocusedRow(ctrl, shift);
+ // TODO needs scrollintoview ?
+ sendSelectedRows();
+ } else {
+ // scroll down by pixels and return, to wait for
+ // new rows, then select the last item in the
+ // viewport
+ selectLastItemInNextRender = true;
+ multiselectPending = shift;
+ scrollByPagelenght(1);
+ }
+ }
+ }
+ } else {
+ /* No selections, go page down by scrolling */
+ scrollByPagelenght(1);
+ }
+ return true;
+ }
+
+ // Page Up navigation
+ if (keycode == getNavigationPageUpKey()) {
+ if (isSelectable()) {
+ /*
+ * If selectable we plagiate MSW behaviour: first scroll to the
+ * end of current view. If at the end, scroll down one page
+ * length and keep the selected row in the bottom part of
+ * visible area.
+ */
+ if (!isFocusAtTheBeginningOfTable()) {
+ VScrollTableRow firstVisibleRowInViewPort = scrollBody
+ .getRowByRowIndex(firstRowInViewPort);
+ if (firstVisibleRowInViewPort != null
+ && firstVisibleRowInViewPort != focusedRow) {
+ // focus is not at the beginning of the table, move
+ // focus and select the first visible row
+ setRowFocus(firstVisibleRowInViewPort);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ } else {
+ int indexOfToBeFocused = focusedRow.getIndex()
+ - getFullyVisibleRowCount();
+ if (indexOfToBeFocused < 0) {
+ indexOfToBeFocused = 0;
+ }
+ VScrollTableRow toBeFocusedRow = scrollBody
+ .getRowByRowIndex(indexOfToBeFocused);
+
+ if (toBeFocusedRow != null) { // if the next focused row
+ // is rendered
+ setRowFocus(toBeFocusedRow);
+ selectFocusedRow(ctrl, shift);
+ // TODO needs scrollintoview ?
+ sendSelectedRows();
+ } else {
+ // unless waiting for the next rowset already
+ // scroll down by pixels and return, to wait for
+ // new rows, then select the last item in the
+ // viewport
+ selectFirstItemInNextRender = true;
+ multiselectPending = shift;
+ scrollByPagelenght(-1);
+ }
+ }
+ }
+ } else {
+ /* No selections, go page up by scrolling */
+ scrollByPagelenght(-1);
+ }
+
+ return true;
+ }
+
+ // Goto start navigation
+ if (keycode == getNavigationStartKey()) {
+ scrollBodyPanel.setScrollPosition(0);
+ if (isSelectable()) {
+ if (focusedRow != null && focusedRow.getIndex() == 0) {
+ return false;
+ } else {
+ VScrollTableRow rowByRowIndex = (VScrollTableRow) scrollBody
+ .iterator().next();
+ if (rowByRowIndex.getIndex() == 0) {
+ setRowFocus(rowByRowIndex);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ } else {
+ // first row of table will come in next row fetch
+ if (ctrl) {
+ focusFirstItemInNextRender = true;
+ } else {
+ selectFirstItemInNextRender = true;
+ multiselectPending = shift;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ // Goto end navigation
+ if (keycode == getNavigationEndKey()) {
+ scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight());
+ if (isSelectable()) {
+ final int lastRendered = scrollBody.getLastRendered();
+ if (lastRendered + 1 == totalRows) {
+ VScrollTableRow rowByRowIndex = scrollBody
+ .getRowByRowIndex(lastRendered);
+ if (focusedRow != rowByRowIndex) {
+ setRowFocus(rowByRowIndex);
+ selectFocusedRow(ctrl, shift);
+ sendSelectedRows();
+ }
+ } else {
+ if (ctrl) {
+ focusLastItemInNextRender = true;
+ } else {
+ selectLastItemInNextRender = true;
+ multiselectPending = shift;
+ }
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isFocusAtTheBeginningOfTable() {
+ return focusedRow.getIndex() == 0;
+ }
+
+ private boolean isFocusAtTheEndOfTable() {
+ return focusedRow.getIndex() + 1 >= totalRows;
+ }
+
+ private int getFullyVisibleRowCount() {
+ return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody
+ .getRowHeight());
+ }
+
+ private void scrollByPagelenght(int i) {
+ int pixels = i * scrollBodyPanel.getOffsetHeight();
+ int newPixels = scrollBodyPanel.getScrollPosition() + pixels;
+ if (newPixels < 0) {
+ newPixels = 0;
+ } // else if too high, NOP (all know browsers accept illegally big
+ // values here)
+ scrollBodyPanel.setScrollPosition(newPixels);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ if (isFocusable()) {
+ hasFocus = true;
+
+ // Focus a row if no row is in focus
+ if (focusedRow == null) {
+ focusRowFromBody();
+ } else {
+ setRowFocus(focusedRow);
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ hasFocus = false;
+ navKeyDown = false;
+
+ if (BrowserInfo.get().isIE()) {
+ // IE sometimes moves focus to a clicked table cell...
+ Element focusedElement = Util.getIEFocusedElement();
+ if (Util.getConnectorForElement(client, getParent(), focusedElement) == this) {
+ // ..in that case, steal the focus back to the focus handler
+ // but not if focus is in a child component instead (#7965)
+ focus();
+ return;
+ }
+ }
+
+ if (isFocusable()) {
+ // Unfocus any row
+ setRowFocus(null);
+ }
+ }
+
+ /**
+ * Removes a key from a range if the key is found in a selected range
+ *
+ * @param key
+ * The key to remove
+ */
+ private void removeRowFromUnsentSelectionRanges(VScrollTableRow row) {
+ Collection<SelectionRange> newRanges = null;
+ for (Iterator<SelectionRange> iterator = selectedRowRanges.iterator(); iterator
+ .hasNext();) {
+ SelectionRange range = iterator.next();
+ if (range.inRange(row)) {
+ // Split the range if given row is in range
+ Collection<SelectionRange> splitranges = range.split(row);
+ if (newRanges == null) {
+ newRanges = new ArrayList<SelectionRange>();
+ }
+ newRanges.addAll(splitranges);
+ iterator.remove();
+ }
+ }
+ if (newRanges != null) {
+ selectedRowRanges.addAll(newRanges);
+ }
+ }
+
+ /**
+ * Can the Table be focused?
+ *
+ * @return True if the table can be focused, else false
+ */
+ public boolean isFocusable() {
+ if (scrollBody != null && enabled) {
+ return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable());
+ }
+ return false;
+ }
+
+ private boolean hasHorizontalScrollbar() {
+ return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth();
+ }
+
+ private boolean hasVerticalScrollbar() {
+ return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Focusable#focus()
+ */
+
+ @Override
+ public void focus() {
+ if (isFocusable()) {
+ scrollBodyPanel.focus();
+ }
+ }
+
+ /**
+ * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the
+ * component).
+ *
+ * If the component has no explicit tabIndex a zero is given (default
+ * tabbing order based on dom hierarchy) or -1 if the component does not
+ * need to gain focus. The component needs no focus if it has no scrollabars
+ * (not scrollable) and not selectable. Note that in the future shortcut
+ * actions may need focus.
+ *
+ */
+ void setProperTabIndex() {
+ int storedScrollTop = 0;
+ int storedScrollLeft = 0;
+
+ if (BrowserInfo.get().getOperaVersion() >= 11) {
+ // Workaround for Opera scroll bug when changing tabIndex (#6222)
+ storedScrollTop = scrollBodyPanel.getScrollPosition();
+ storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition();
+ }
+
+ if (tabIndex == 0 && !isFocusable()) {
+ scrollBodyPanel.setTabIndex(-1);
+ } else {
+ scrollBodyPanel.setTabIndex(tabIndex);
+ }
+
+ if (BrowserInfo.get().getOperaVersion() >= 11) {
+ // Workaround for Opera scroll bug when changing tabIndex (#6222)
+ scrollBodyPanel.setScrollPosition(storedScrollTop);
+ scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft);
+ }
+ }
+
+ public void startScrollingVelocityTimer() {
+ if (scrollingVelocityTimer == null) {
+ scrollingVelocityTimer = new Timer() {
+
+ @Override
+ public void run() {
+ scrollingVelocity++;
+ }
+ };
+ scrollingVelocityTimer.scheduleRepeating(100);
+ }
+ }
+
+ public void cancelScrollingVelocityTimer() {
+ if (scrollingVelocityTimer != null) {
+ // Remove velocityTimer if it exists and the Table is disabled
+ scrollingVelocityTimer.cancel();
+ scrollingVelocityTimer = null;
+ scrollingVelocity = 10;
+ }
+ }
+
+ /**
+ *
+ * @param keyCode
+ * @return true if the given keyCode is used by the table for navigation
+ */
+ private boolean isNavigationKey(int keyCode) {
+ return keyCode == getNavigationUpKey()
+ || keyCode == getNavigationLeftKey()
+ || keyCode == getNavigationRightKey()
+ || keyCode == getNavigationDownKey()
+ || keyCode == getNavigationPageUpKey()
+ || keyCode == getNavigationPageDownKey()
+ || keyCode == getNavigationEndKey()
+ || keyCode == getNavigationStartKey();
+ }
+
+ public void lazyRevertFocusToRow(final VScrollTableRow currentlyFocusedRow) {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (currentlyFocusedRow != null) {
+ setRowFocus(currentlyFocusedRow);
+ } else {
+ VConsole.log("no row?");
+ focusRowFromBody();
+ }
+ scrollBody.ensureFocus();
+ }
+ });
+ }
+
+ @Override
+ public Action[] getActions() {
+ if (bodyActionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[bodyActionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = bodyActionKeys[i];
+ Action bodyAction = new TreeAction(this, null, actionKey);
+ bodyAction.setCaption(getActionCaption(actionKey));
+ bodyAction.setIconUrl(getActionIcon(actionKey));
+ actions[i] = bodyAction;
+ }
+ return actions;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /**
+ * Add this to the element mouse down event by using element.setPropertyJSO
+ * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
+ * when the mouse is depressed in the mouse up event.
+ *
+ * @return Returns the JSO preventing text selection
+ */
+ private static native JavaScriptObject getPreventTextSelectionIEHack()
+ /*-{
+ return function(){ return false; };
+ }-*/;
+
+ public void triggerLazyColumnAdjustment(boolean now) {
+ lazyAdjustColumnWidths.cancel();
+ if (now) {
+ lazyAdjustColumnWidths.run();
+ } else {
+ lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT);
+ }
+ }
+
+ private boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ private boolean isDynamicHeight() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ if (paintable == null) {
+ // This should be refactored. As isDynamicHeight can be called from
+ // a timer it is possible that the connector has been unregistered
+ // when this method is called, causing getConnector to return null.
+ return false;
+ }
+ return paintable.isUndefinedHeight();
+ }
+
+ private void debug(String msg) {
+ if (enableDebug) {
+ VConsole.error(msg);
+ }
+ }
+
+ public Widget getWidgetForPaintable() {
+ return this;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java
new file mode 100644
index 0000000000..ea0bea6b04
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetBaseConnector.java
@@ -0,0 +1,100 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+
+public abstract class TabsheetBaseConnector extends
+ AbstractComponentContainerConnector implements Paintable {
+
+ public static final String ATTRIBUTE_TAB_DISABLED = "disabled";
+ public static final String ATTRIBUTE_TAB_DESCRIPTION = "description";
+ public static final String ATTRIBUTE_TAB_ERROR_MESSAGE = "error";
+ public static final String ATTRIBUTE_TAB_CAPTION = "caption";
+ public static final String ATTRIBUTE_TAB_ICON = "icon";
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().client = client;
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // Update member references
+ getWidget().id = uidl.getId();
+ getWidget().disabled = !isEnabled();
+
+ // Render content
+ final UIDL tabs = uidl.getChildUIDL(0);
+
+ // Widgets in the TabSheet before update
+ ArrayList<Widget> oldWidgets = new ArrayList<Widget>();
+ for (Iterator<Widget> iterator = getWidget().getWidgetIterator(); iterator
+ .hasNext();) {
+ oldWidgets.add(iterator.next());
+ }
+
+ // Clear previous values
+ getWidget().tabKeys.clear();
+ getWidget().disabledTabKeys.clear();
+
+ int index = 0;
+ for (final Iterator<Object> 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(ATTRIBUTE_TAB_DISABLED)) {
+ getWidget().disabledTabKeys.add(key);
+ }
+
+ getWidget().tabKeys.add(key);
+
+ if (selected) {
+ getWidget().activeTabIndex = index;
+ }
+ getWidget().renderTab(tab, index, selected, hidden);
+ index++;
+ }
+
+ int tabCount = getWidget().getTabCount();
+ while (tabCount-- > index) {
+ getWidget().removeTab(index);
+ }
+
+ for (int i = 0; i < getWidget().getTabCount(); i++) {
+ ComponentConnector p = getWidget().getTab(i);
+ // null for PlaceHolder widgets
+ if (p != null) {
+ oldWidgets.remove(p.getWidget());
+ }
+ }
+
+ // Detach any old tab widget, should be max 1
+ for (Iterator<Widget> iterator = oldWidgets.iterator(); iterator
+ .hasNext();) {
+ Widget oldWidget = iterator.next();
+ if (oldWidget.isAttached()) {
+ oldWidget.removeFromParent();
+ }
+ }
+
+ }
+
+ @Override
+ public VTabsheetBase getWidget() {
+ return (VTabsheetBase) super.getWidget();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java
new file mode 100644
index 0000000000..ce19f1e02a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/TabsheetConnector.java
@@ -0,0 +1,128 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+import com.vaadin.ui.TabSheet;
+
+@Connect(TabSheet.class)
+public class TabsheetConnector extends TabsheetBaseConnector implements
+ SimpleManagedLayout, MayScrollChildren {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ if (isRealUpdate(uidl)) {
+ // Handle stylename changes before generics (might affect size
+ // calculations)
+ getWidget().handleStyleNames(uidl, getState());
+ }
+
+ super.updateFromUIDL(uidl, client);
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // tabs; push or not
+ if (!isUndefinedWidth()) {
+ DOM.setStyleAttribute(getWidget().tabs, "overflow", "hidden");
+ } else {
+ getWidget().showAllTabs();
+ DOM.setStyleAttribute(getWidget().tabs, "width", "");
+ DOM.setStyleAttribute(getWidget().tabs, "overflow", "visible");
+ getWidget().updateDynamicWidth();
+ }
+
+ if (!isUndefinedHeight()) {
+ // Must update height after the styles have been set
+ getWidget().updateContentNodeHeight();
+ getWidget().updateOpenTabSize();
+ }
+
+ getWidget().iLayout();
+
+ // Re run relative size update to ensure optimal scrollbars
+ // TODO isolate to situation that visible tab has undefined height
+ try {
+ client.handleComponentRelativeSize(getWidget().tp
+ .getWidget(getWidget().tp.getVisibleWidget()));
+ } catch (Exception e) {
+ // Ignore, most likely empty tabsheet
+ }
+
+ getWidget().waitingForResponse = false;
+ }
+
+ @Override
+ public VTabsheet getWidget() {
+ return (VTabsheet) super.getWidget();
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ /* Tabsheet does not render its children's captions */
+ }
+
+ @Override
+ public void layout() {
+ VTabsheet tabsheet = getWidget();
+
+ tabsheet.updateContentNodeHeight();
+
+ if (isUndefinedWidth()) {
+ tabsheet.contentNode.getStyle().setProperty("width", "");
+ } else {
+ int contentWidth = tabsheet.getOffsetWidth()
+ - tabsheet.getContentAreaBorderWidth();
+ if (contentWidth < 0) {
+ contentWidth = 0;
+ }
+ tabsheet.contentNode.getStyle().setProperty("width",
+ contentWidth + "px");
+ }
+
+ tabsheet.updateOpenTabSize();
+ if (isUndefinedWidth()) {
+ tabsheet.updateDynamicWidth();
+ }
+
+ tabsheet.iLayout();
+
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+
+ TooltipInfo info = null;
+
+ // Find a tooltip for the tab, if the element is a tab
+ if (element != getWidget().getElement()) {
+ Object node = Util.findWidget(
+ (com.google.gwt.user.client.Element) element,
+ VTabsheet.TabCaption.class);
+
+ if (node != null) {
+ VTabsheet.TabCaption caption = (VTabsheet.TabCaption) node;
+ info = caption.getTooltipInfo();
+ }
+ }
+
+ // If not tab tooltip was found, use the default
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java
new file mode 100644
index 0000000000..1f6314050e
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheet.java
@@ -0,0 +1,1238 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.shared.EventId;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VCaption;
+import com.vaadin.terminal.gwt.client.ui.label.VLabel;
+
+public class VTabsheet extends VTabsheetBase implements Focusable,
+ FocusHandler, BlurHandler, KeyDownHandler {
+
+ private static class VCloseEvent {
+ private Tab tab;
+
+ VCloseEvent(Tab tab) {
+ this.tab = tab;
+ }
+
+ public Tab getTab() {
+ return tab;
+ }
+
+ }
+
+ private interface VCloseHandler {
+ public void onClose(VCloseEvent event);
+ }
+
+ /**
+ * Representation of a single "tab" shown in the TabBar
+ *
+ */
+ private static class Tab extends SimplePanel implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers {
+ private static final String TD_CLASSNAME = CLASSNAME + "-tabitemcell";
+ private static final String TD_FIRST_CLASSNAME = TD_CLASSNAME
+ + "-first";
+ private static final String TD_SELECTED_CLASSNAME = TD_CLASSNAME
+ + "-selected";
+ private static final String TD_SELECTED_FIRST_CLASSNAME = TD_SELECTED_CLASSNAME
+ + "-first";
+ private static final String TD_DISABLED_CLASSNAME = TD_CLASSNAME
+ + "-disabled";
+
+ private static final String DIV_CLASSNAME = CLASSNAME + "-tabitem";
+ private static final String DIV_SELECTED_CLASSNAME = DIV_CLASSNAME
+ + "-selected";
+
+ private TabCaption tabCaption;
+ Element td = getElement();
+ private VCloseHandler closeHandler;
+
+ private boolean enabledOnServer = true;
+ private Element div;
+ private TabBar tabBar;
+ private boolean hiddenOnServer = false;
+
+ private String styleName;
+
+ private Tab(TabBar tabBar) {
+ super(DOM.createTD());
+ this.tabBar = tabBar;
+ setStyleName(td, TD_CLASSNAME);
+
+ div = DOM.createDiv();
+ focusImpl.setTabIndex(td, -1);
+ setStyleName(div, DIV_CLASSNAME);
+
+ DOM.appendChild(td, div);
+
+ tabCaption = new TabCaption(this, getTabsheet()
+ .getApplicationConnection());
+ add(tabCaption);
+
+ addFocusHandler(getTabsheet());
+ addBlurHandler(getTabsheet());
+ addKeyDownHandler(getTabsheet());
+ }
+
+ public boolean isHiddenOnServer() {
+ return hiddenOnServer;
+ }
+
+ public void setHiddenOnServer(boolean hiddenOnServer) {
+ this.hiddenOnServer = hiddenOnServer;
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ // Attach caption element to div, not td
+ return div;
+ }
+
+ public boolean isEnabledOnServer() {
+ return enabledOnServer;
+ }
+
+ public void setEnabledOnServer(boolean enabled) {
+ enabledOnServer = enabled;
+ setStyleName(td, TD_DISABLED_CLASSNAME, !enabled);
+ if (!enabled) {
+ focusImpl.setTabIndex(td, -1);
+ }
+ }
+
+ public void addClickHandler(ClickHandler handler) {
+ tabCaption.addClickHandler(handler);
+ }
+
+ public void setCloseHandler(VCloseHandler closeHandler) {
+ this.closeHandler = closeHandler;
+ }
+
+ /**
+ * Toggles the style names for the Tab
+ *
+ * @param selected
+ * true if the Tab is selected
+ * @param first
+ * true if the Tab is the first visible Tab
+ */
+ public void setStyleNames(boolean selected, boolean first) {
+ setStyleName(td, TD_FIRST_CLASSNAME, first);
+ setStyleName(td, TD_SELECTED_CLASSNAME, selected);
+ setStyleName(td, TD_SELECTED_FIRST_CLASSNAME, selected && first);
+ setStyleName(div, DIV_SELECTED_CLASSNAME, selected);
+ }
+
+ public void setTabulatorIndex(int tabIndex) {
+ focusImpl.setTabIndex(td, tabIndex);
+ }
+
+ public boolean isClosable() {
+ return tabCaption.isClosable();
+ }
+
+ public void onClose() {
+ closeHandler.onClose(new VCloseEvent(this));
+ }
+
+ public VTabsheet getTabsheet() {
+ return tabBar.getTabsheet();
+ }
+
+ public void updateFromUIDL(UIDL tabUidl) {
+ tabCaption.updateCaption(tabUidl);
+
+ // Apply the styleName set for the tab
+ String newStyleName = tabUidl.getStringAttribute(TAB_STYLE_NAME);
+ // Find the nth td element
+ if (newStyleName != null && newStyleName.length() != 0) {
+ if (!newStyleName.equals(styleName)) {
+ // If we have a new style name
+ if (styleName != null && styleName.length() != 0) {
+ // Remove old style name if present
+ td.removeClassName(TD_CLASSNAME + "-" + styleName);
+ }
+ // Set new style name
+ td.addClassName(TD_CLASSNAME + "-" + newStyleName);
+ styleName = newStyleName;
+ }
+ } else if (styleName != null) {
+ // Remove the set stylename if no stylename is present in the
+ // uidl
+ td.removeClassName(TD_CLASSNAME + "-" + styleName);
+ styleName = null;
+ }
+ }
+
+ public void recalculateCaptionWidth() {
+ tabCaption.setWidth(tabCaption.getRequiredWidth() + "px");
+ }
+
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ public void focus() {
+ focusImpl.focus(td);
+ }
+
+ public void blur() {
+ focusImpl.blur(td);
+ }
+ }
+
+ public static class TabCaption extends VCaption {
+
+ private boolean closable = false;
+ private Element closeButton;
+ private Tab tab;
+ private ApplicationConnection client;
+
+ TabCaption(Tab tab, ApplicationConnection client) {
+ super(client);
+ this.client = client;
+ this.tab = tab;
+ }
+
+ public boolean updateCaption(UIDL uidl) {
+ if (uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION)) {
+ setTooltipInfo(new TooltipInfo(
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION),
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE)));
+ } else {
+ setTooltipInfo(null);
+ }
+
+ // TODO need to call this instead of super because the caption does
+ // not have an owner
+ boolean ret = updateCaptionWithoutOwner(
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION),
+ uidl.hasAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE),
+ uidl.getStringAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON));
+
+ setClosable(uidl.hasAttribute("closable"));
+
+ return ret;
+ }
+
+ private VTabsheet getTabsheet() {
+ return tab.getTabsheet();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (closable && event.getTypeInt() == Event.ONCLICK
+ && event.getEventTarget().cast() == closeButton) {
+ tab.onClose();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ super.onBrowserEvent(event);
+
+ if (event.getTypeInt() == Event.ONLOAD) {
+ getTabsheet().tabSizeMightHaveChanged(getTab());
+ }
+ }
+
+ public Tab getTab() {
+ return tab;
+ }
+
+ public void setClosable(boolean closable) {
+ this.closable = closable;
+ if (closable && closeButton == null) {
+ closeButton = DOM.createSpan();
+ closeButton.setInnerHTML("&times;");
+ closeButton
+ .setClassName(VTabsheet.CLASSNAME + "-caption-close");
+ getElement().insertBefore(closeButton,
+ getElement().getLastChild());
+ } else if (!closable && closeButton != null) {
+ getElement().removeChild(closeButton);
+ closeButton = null;
+ }
+ if (closable) {
+ addStyleDependentName("closable");
+ } else {
+ removeStyleDependentName("closable");
+ }
+ }
+
+ public boolean isClosable() {
+ return closable;
+ }
+
+ @Override
+ public int getRequiredWidth() {
+ int width = super.getRequiredWidth();
+ if (closeButton != null) {
+ width += Util.getRequiredWidth(closeButton);
+ }
+ return width;
+ }
+
+ public Element getCloseButton() {
+ return closeButton;
+ }
+
+ }
+
+ static class TabBar extends ComplexPanel implements ClickHandler,
+ VCloseHandler {
+
+ private final Element tr = DOM.createTR();
+
+ private final Element spacerTd = DOM.createTD();
+
+ private Tab selected;
+
+ private VTabsheet tabsheet;
+
+ TabBar(VTabsheet tabsheet) {
+ this.tabsheet = tabsheet;
+
+ 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);
+ }
+
+ @Override
+ public void onClose(VCloseEvent event) {
+ Tab tab = event.getTab();
+ if (!tab.isEnabledOnServer()) {
+ return;
+ }
+ int tabIndex = getWidgetIndex(tab);
+ getTabsheet().sendTabClosedEvent(tabIndex);
+ }
+
+ protected Element getContainerElement() {
+ return tr;
+ }
+
+ public int getTabCount() {
+ return getWidgetCount();
+ }
+
+ public Tab addTab() {
+ Tab t = new Tab(this);
+ int tabIndex = getTabCount();
+
+ // Logical attach
+ insert(t, tr, tabIndex, true);
+
+ if (tabIndex == 0) {
+ // Set the "first" style
+ t.setStyleNames(false, true);
+ }
+
+ t.addClickHandler(this);
+ t.setCloseHandler(this);
+
+ return t;
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ TabCaption caption = (TabCaption) event.getSource();
+ Element targetElement = event.getNativeEvent().getEventTarget()
+ .cast();
+ // the tab should not be focused if the close button was clicked
+ if (targetElement == caption.getCloseButton()) {
+ return;
+ }
+
+ int index = getWidgetIndex(caption.getParent());
+ // IE needs explicit focus()
+ if (BrowserInfo.get().isIE()) {
+ getTabsheet().focus();
+ }
+ getTabsheet().onTabSelected(index);
+ }
+
+ public VTabsheet getTabsheet() {
+ return tabsheet;
+ }
+
+ public Tab getTab(int index) {
+ if (index < 0 || index >= getTabCount()) {
+ return null;
+ }
+ return (Tab) super.getWidget(index);
+ }
+
+ public void selectTab(int index) {
+ final Tab newSelected = getTab(index);
+ final Tab oldSelected = selected;
+
+ newSelected.setStyleNames(true, isFirstVisibleTab(index));
+ newSelected.setTabulatorIndex(getTabsheet().tabulatorIndex);
+
+ if (oldSelected != null && oldSelected != newSelected) {
+ oldSelected.setStyleNames(false,
+ isFirstVisibleTab(getWidgetIndex(oldSelected)));
+ oldSelected.setTabulatorIndex(-1);
+ }
+
+ // Update the field holding the currently selected tab
+ selected = newSelected;
+
+ // The selected tab might need more (or less) space
+ newSelected.recalculateCaptionWidth();
+ getTab(tabsheet.activeTabIndex).recalculateCaptionWidth();
+ }
+
+ public void removeTab(int i) {
+ Tab tab = getTab(i);
+ if (tab == null) {
+ return;
+ }
+
+ remove(tab);
+
+ /*
+ * If this widget was selected we need to unmark it as the last
+ * selected
+ */
+ if (tab == selected) {
+ selected = null;
+ }
+
+ // FIXME: Shouldn't something be selected instead?
+ }
+
+ private boolean isFirstVisibleTab(int index) {
+ return getFirstVisibleTab() == index;
+ }
+
+ /**
+ * Returns the index of the first visible tab
+ *
+ * @return
+ */
+ private int getFirstVisibleTab() {
+ return getNextVisibleTab(-1);
+ }
+
+ /**
+ * Find the next visible tab. Returns -1 if none is found.
+ *
+ * @param i
+ * @return
+ */
+ private int getNextVisibleTab(int i) {
+ int tabs = getTabCount();
+ do {
+ i++;
+ } while (i < tabs && getTab(i).isHiddenOnServer());
+
+ if (i == tabs) {
+ return -1;
+ } else {
+ return i;
+ }
+ }
+
+ /**
+ * Find the previous visible tab. Returns -1 if none is found.
+ *
+ * @param i
+ * @return
+ */
+ private int getPreviousVisibleTab(int i) {
+ do {
+ i--;
+ } while (i >= 0 && getTab(i).isHiddenOnServer());
+
+ return i;
+
+ }
+
+ public int scrollLeft(int currentFirstVisible) {
+ int prevVisible = getPreviousVisibleTab(currentFirstVisible);
+ if (prevVisible == -1) {
+ return -1;
+ }
+
+ Tab newFirst = getTab(prevVisible);
+ newFirst.setVisible(true);
+ newFirst.recalculateCaptionWidth();
+
+ return prevVisible;
+ }
+
+ public int scrollRight(int currentFirstVisible) {
+ int nextVisible = getNextVisibleTab(currentFirstVisible);
+ if (nextVisible == -1) {
+ return -1;
+ }
+ Tab currentFirst = getTab(currentFirstVisible);
+ currentFirst.setVisible(false);
+ currentFirst.recalculateCaptionWidth();
+ return nextVisible;
+ }
+ }
+
+ public static final String CLASSNAME = "v-tabsheet";
+
+ public static final String TABS_CLASSNAME = "v-tabsheet-tabcontainer";
+ public static final String SCROLLER_CLASSNAME = "v-tabsheet-scroller";
+
+ // Can't use "style" as it's already in use
+ public static final String TAB_STYLE_NAME = "tabstyle";
+
+ final Element tabs; // tabbar and 'scroller' container
+ Tab focusedTab;
+ /**
+ * The tabindex property (position in the browser's focus cycle.) Named like
+ * this to avoid confusion with activeTabIndex.
+ */
+ int tabulatorIndex = 0;
+
+ private static final FocusImpl focusImpl = FocusImpl.getFocusImplForPanel();
+
+ 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
+
+ /**
+ * The index of the first visible tab (when scrolled)
+ */
+ private int scrollerIndex = 0;
+
+ final TabBar tb = new TabBar(this);
+ final VTabsheetPanel tp = new VTabsheetPanel();
+ final Element contentNode;
+
+ private final Element deco;
+
+ boolean waitingForResponse;
+
+ private String currentStyle;
+
+ /**
+ * @return Whether the tab could be selected or not.
+ */
+ private boolean onTabSelected(final int tabIndex) {
+ Tab tab = tb.getTab(tabIndex);
+ if (client == null || disabled || waitingForResponse) {
+ return false;
+ }
+ if (!tab.isEnabledOnServer() || tab.isHiddenOnServer()) {
+ return false;
+ }
+ if (activeTabIndex != tabIndex) {
+ tb.selectTab(tabIndex);
+
+ // If this TabSheet already has focus, set the new selected tab
+ // as focused.
+ if (focusedTab != null) {
+ focusedTab = tab;
+ }
+
+ addStyleDependentName("loading");
+ // Hide the current contents so a loading indicator can be shown
+ // instead
+ Widget currentlyDisplayedWidget = tp.getWidget(tp
+ .getVisibleWidget());
+ currentlyDisplayedWidget.getElement().getParentElement().getStyle()
+ .setVisibility(Visibility.HIDDEN);
+ client.updateVariable(id, "selected", tabKeys.get(tabIndex)
+ .toString(), true);
+ waitingForResponse = true;
+ }
+ // Note that we return true when tabIndex == activeTabIndex; the active
+ // tab could be selected, it's just a no-op.
+ return true;
+ }
+
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ public void tabSizeMightHaveChanged(Tab tab) {
+ // icon onloads may change total width of tabsheet
+ if (isDynamicWidth()) {
+ updateDynamicWidth();
+ }
+ updateTabScroller();
+
+ }
+
+ void sendTabClosedEvent(int tabIndex) {
+ client.updateVariable(id, "close", tabKeys.get(tabIndex), true);
+ }
+
+ boolean isDynamicWidth() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedWidth();
+ }
+
+ boolean isDynamicHeight() {
+ ComponentConnector paintable = ConnectorMap.get(client).getConnector(
+ this);
+ return paintable.isUndefinedHeight();
+ }
+
+ public VTabsheet() {
+ super(CLASSNAME);
+
+ addHandler(this, FocusEvent.getType());
+ addHandler(this, BlurEvent.getType());
+
+ // 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) {
+ if (event.getTypeInt() == Event.ONCLICK) {
+ // Tab scrolling
+ if (isScrolledTabs() && DOM.eventGetTarget(event) == scrollerPrev) {
+ int newFirstIndex = tb.scrollLeft(scrollerIndex);
+ if (newFirstIndex != -1) {
+ scrollerIndex = newFirstIndex;
+ updateTabScroller();
+ }
+ return;
+ } else if (isClippedTabs()
+ && DOM.eventGetTarget(event) == scrollerNext) {
+ int newFirstIndex = tb.scrollRight(scrollerIndex);
+
+ if (newFirstIndex != -1) {
+ scrollerIndex = newFirstIndex;
+ updateTabScroller();
+ }
+ return;
+ }
+ }
+ super.onBrowserEvent(event);
+ }
+
+ /**
+ * Checks if the tab with the selected index has been scrolled out of the
+ * view (on the left side).
+ *
+ * @param index
+ * @return
+ */
+ private boolean scrolledOutOfView(int index) {
+ return scrollerIndex > index;
+ }
+
+ void handleStyleNames(UIDL uidl, ComponentState state) {
+ // Add proper stylenames for all elements (easier to prevent unwanted
+ // style inheritance)
+ if (state.hasStyles()) {
+ final List<String> styles = state.getStyles();
+ if (!currentStyle.equals(styles.toString())) {
+ currentStyle = styles.toString();
+ final String tabsBaseClass = TABS_CLASSNAME;
+ String tabsClass = tabsBaseClass;
+ final String contentBaseClass = CLASSNAME + "-content";
+ String contentClass = contentBaseClass;
+ final String decoBaseClass = CLASSNAME + "-deco";
+ String decoClass = decoBaseClass;
+ for (String style : styles) {
+ tb.addStyleDependentName(style);
+ tabsClass += " " + tabsBaseClass + "-" + style;
+ contentClass += " " + contentBaseClass + "-" + style;
+ decoClass += " " + decoBaseClass + "-" + style;
+ }
+ DOM.setElementProperty(tabs, "className", tabsClass);
+ DOM.setElementProperty(contentNode, "className", contentClass);
+ DOM.setElementProperty(deco, "className", decoClass);
+ borderW = -1;
+ }
+ } else {
+ tb.setStyleName(CLASSNAME + "-tabs");
+ DOM.setElementProperty(tabs, "className", TABS_CLASSNAME);
+ 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");
+ }
+ }
+
+ void updateDynamicWidth() {
+ // Find width consumed by tabs
+ TableCellElement spacerCell = ((TableElement) tb.getElement().cast())
+ .getRows().getItem(0).getCells().getItem(tb.getTabCount());
+
+ int spacerWidth = spacerCell.getOffsetWidth();
+ DivElement div = (DivElement) spacerCell.getFirstChildElement();
+
+ int spacerMinWidth = spacerCell.getOffsetWidth() - div.getOffsetWidth();
+
+ int tabsWidth = tb.getOffsetWidth() - spacerWidth + spacerMinWidth;
+
+ // Find content width
+ Style style = tp.getElement().getStyle();
+ String overflow = style.getProperty("overflow");
+ style.setProperty("overflow", "hidden");
+ style.setPropertyPx("width", tabsWidth);
+
+ boolean hasTabs = tp.getWidgetCount() > 0;
+
+ Style wrapperstyle = null;
+ if (hasTabs) {
+ wrapperstyle = tp.getWidget(tp.getVisibleWidget()).getElement()
+ .getParentElement().getStyle();
+ wrapperstyle.setPropertyPx("width", tabsWidth);
+ }
+ // Get content width from actual widget
+
+ int contentWidth = 0;
+ if (hasTabs) {
+ 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);
+ if (hasTabs) {
+ 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) {
+ Tab tab = tb.getTab(index);
+ if (tab == null) {
+ tab = tb.addTab();
+ }
+ tab.updateFromUIDL(tabUidl);
+ tab.setEnabledOnServer((!disabledTabKeys.contains(tabKeys.get(index))));
+ tab.setHiddenOnServer(hidden);
+
+ if (scrolledOutOfView(index)) {
+ // Should not set tabs visible if they are scrolled out of view
+ hidden = true;
+ }
+ // Set the current visibility of the tab (in the browser)
+ tab.setVisible(!hidden);
+
+ /*
+ * Force the width of the caption container so the content will not wrap
+ * and tabs won't be too narrow in certain browsers
+ */
+ tab.recalculateCaptionWidth();
+
+ UIDL tabContentUIDL = null;
+ ComponentConnector tabContentPaintable = null;
+ Widget tabContentWidget = null;
+ if (tabUidl.getChildCount() > 0) {
+ tabContentUIDL = tabUidl.getChildUIDL(0);
+ tabContentPaintable = client.getPaintable(tabContentUIDL);
+ tabContentWidget = tabContentPaintable.getWidget();
+ }
+
+ if (tabContentPaintable != null) {
+ /* This is a tab with content information */
+
+ int oldIndex = tp.getWidgetIndex(tabContentWidget);
+ 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(tabContentWidget, 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(tabContentWidget) < 0) {
+ tp.insert(tabContentWidget, index);
+ }
+ } else if (tp.getWidgetCount() <= index) {
+ tp.add(new PlaceHolder());
+ }
+ }
+ }
+
+ public class PlaceHolder extends VLabel {
+ 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 ComponentConnector content = client.getPaintable(contentUIDL);
+ Widget newWidget = content.getWidget();
+ if (tp.getWidgetCount() > activeTabIndex) {
+ Widget old = tp.getWidget(activeTabIndex);
+ if (old != newWidget) {
+ tp.remove(activeTabIndex);
+ ConnectorMap paintableMap = ConnectorMap.get(client);
+ if (paintableMap.isConnector(old)) {
+ paintableMap.unregisterConnector(paintableMap
+ .getConnector(old));
+ }
+ tp.insert(content.getWidget(), activeTabIndex);
+ }
+ } else {
+ tp.add(content.getWidget());
+ }
+
+ tp.showWidget(activeTabIndex);
+
+ VTabsheet.this.iLayout();
+ /*
+ * The size of a cached, relative sized component must be updated to
+ * report correct size to updateOpenTabSize().
+ */
+ if (contentUIDL.getBooleanAttribute("cached")) {
+ client.handleComponentRelativeSize(content.getWidget());
+ }
+ updateOpenTabSize();
+ VTabsheet.this.removeStyleDependentName("loading");
+ }
+
+ void updateContentNodeHeight() {
+ if (!isDynamicHeight()) {
+ 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");
+ } else {
+ DOM.setStyleAttribute(contentNode, "height", "");
+ }
+ }
+
+ public void iLayout() {
+ updateTabScroller();
+ }
+
+ /**
+ * 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.
+ */
+ 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 = contentNode.getOffsetHeight();
+ }
+ if (!isDynamicWidth()) {
+ width = contentNode.getOffsetWidth() - getContentAreaBorderWidth();
+ } 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 (!isDynamicWidth()) {
+ ComponentConnector paintable = ConnectorMap.get(client)
+ .getConnector(this);
+ DOM.setStyleAttribute(tabs, "width", paintable.getState()
+ .getWidth());
+ }
+
+ // Make sure scrollerIndex is valid
+ if (scrollerIndex < 0 || scrollerIndex > tb.getTabCount()) {
+ scrollerIndex = tb.getFirstVisibleTab();
+ } else if (tb.getTabCount() > 0
+ && tb.getTab(scrollerIndex).isHiddenOnServer()) {
+ scrollerIndex = tb.getNextVisibleTab(scrollerIndex);
+ }
+
+ boolean scrolled = isScrolledTabs();
+ boolean clipped = isClippedTabs();
+ if (tb.getTabCount() > 0 && 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");
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ style.setProperty("whiteSpace", "");
+ }
+ });
+ }
+
+ }
+
+ void showAllTabs() {
+ scrollerIndex = tb.getFirstVisibleTab();
+ for (int i = 0; i < tb.getTabCount(); i++) {
+ Tab t = tb.getTab(i);
+ if (!t.isHiddenOnServer()) {
+ t.setVisible(true);
+ }
+ }
+ }
+
+ private boolean isScrolledTabs() {
+ return scrollerIndex > tb.getFirstVisibleTab();
+ }
+
+ private boolean isClippedTabs() {
+ return (tb.getOffsetWidth() - DOM.getElementPropertyInt((Element) tb
+ .getContainerElement().getLastChild().cast(), "offsetWidth")) > getOffsetWidth()
+ - (isScrolledTabs() ? scroller.getOffsetWidth() : 0);
+ }
+
+ private boolean isClipped(Tab tab) {
+ return tab.getAbsoluteLeft() + tab.getOffsetWidth() > getAbsoluteLeft()
+ + getOffsetWidth() - scroller.getOffsetWidth();
+ }
+
+ @Override
+ protected void clearPaintables() {
+
+ int i = tb.getTabCount();
+ while (i > 0) {
+ tb.removeTab(--i);
+ }
+ tp.clear();
+
+ }
+
+ @Override
+ protected Iterator<Widget> getWidgetIterator() {
+ return tp.iterator();
+ }
+
+ private int borderW = -1;
+
+ int getContentAreaBorderWidth() {
+ if (borderW < 0) {
+ borderW = Util.measureHorizontalBorder(contentNode);
+ }
+ return borderW;
+ }
+
+ @Override
+ protected int getTabCount() {
+ return tb.getTabCount();
+ }
+
+ @Override
+ protected ComponentConnector getTab(int index) {
+ if (tp.getWidgetCount() > index) {
+ Widget widget = tp.getWidget(index);
+ return ConnectorMap.get(client).getConnector(widget);
+ }
+ 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);
+ }
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ if (focusedTab != null && event.getSource() instanceof Tab) {
+ focusedTab = null;
+ if (client.hasEventListeners(this, EventId.BLUR)) {
+ client.updateVariable(id, EventId.BLUR, "", true);
+ }
+ }
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ if (focusedTab == null && event.getSource() instanceof Tab) {
+ focusedTab = (Tab) event.getSource();
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(id, EventId.FOCUS, "", true);
+ }
+ }
+ }
+
+ @Override
+ public void focus() {
+ tb.getTab(activeTabIndex).focus();
+ }
+
+ public void blur() {
+ tb.getTab(activeTabIndex).blur();
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getSource() instanceof Tab) {
+ int keycode = event.getNativeEvent().getKeyCode();
+
+ if (keycode == getPreviousTabKey()) {
+ selectPreviousTab();
+ } else if (keycode == getNextTabKey()) {
+ selectNextTab();
+ } else if (keycode == getCloseTabKey()) {
+ Tab tab = tb.getTab(activeTabIndex);
+ if (tab.isClosable()) {
+ tab.onClose();
+ }
+ }
+ }
+ }
+
+ /**
+ * @return The key code of the keyboard shortcut that selects the previous
+ * tab in a focused tabsheet.
+ */
+ protected int getPreviousTabKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * @return The key code of the keyboard shortcut that selects the next tab
+ * in a focused tabsheet.
+ */
+ protected int getNextTabKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * @return The key code of the keyboard shortcut that closes the currently
+ * selected tab in a focused tabsheet.
+ */
+ protected int getCloseTabKey() {
+ return KeyCodes.KEY_DELETE;
+ }
+
+ private void selectPreviousTab() {
+ int newTabIndex = activeTabIndex;
+ // Find the previous visible and enabled tab if any.
+ do {
+ newTabIndex--;
+ } while (newTabIndex >= 0 && !onTabSelected(newTabIndex));
+
+ if (newTabIndex >= 0) {
+ activeTabIndex = newTabIndex;
+ if (isScrolledTabs()) {
+ // Scroll until the new active tab is visible
+ int newScrollerIndex = scrollerIndex;
+ while (tb.getTab(activeTabIndex).getAbsoluteLeft() < getAbsoluteLeft()
+ && newScrollerIndex != -1) {
+ newScrollerIndex = tb.scrollLeft(newScrollerIndex);
+ }
+ scrollerIndex = newScrollerIndex;
+ updateTabScroller();
+ }
+ }
+ }
+
+ private void selectNextTab() {
+ int newTabIndex = activeTabIndex;
+ // Find the next visible and enabled tab if any.
+ do {
+ newTabIndex++;
+ } while (newTabIndex < getTabCount() && !onTabSelected(newTabIndex));
+
+ if (newTabIndex < getTabCount()) {
+ activeTabIndex = newTabIndex;
+ if (isClippedTabs()) {
+ // Scroll until the new active tab is completely visible
+ int newScrollerIndex = scrollerIndex;
+ while (isClipped(tb.getTab(activeTabIndex))
+ && newScrollerIndex != -1) {
+ newScrollerIndex = tb.scrollRight(newScrollerIndex);
+ }
+ scrollerIndex = newScrollerIndex;
+ updateTabScroller();
+ }
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java
new file mode 100644
index 0000000000..ed9883dd35
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetBase.java
@@ -0,0 +1,75 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+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.ComponentConnector;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public abstract class VTabsheetBase extends ComplexPanel {
+
+ protected String id;
+ protected ApplicationConnection client;
+
+ protected final ArrayList<String> tabKeys = new ArrayList<String>();
+ protected int activeTabIndex = 0;
+ protected boolean disabled;
+ protected boolean readonly;
+ protected Set<String> disabledTabKeys = new HashSet<String>();
+
+ public VTabsheetBase(String classname) {
+ setElement(DOM.createDiv());
+ setStyleName(classname);
+ }
+
+ /**
+ * @return a list of currently shown Widgets
+ */
+ abstract protected Iterator<Widget> getWidgetIterator();
+
+ /**
+ * 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 ComponentConnector 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/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java
new file mode 100644
index 0000000000..bd6cddb682
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tabsheet/VTabsheetPanel.java
@@ -0,0 +1,189 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.tabsheet;
+
+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.ui.TouchScrollDelegate;
+import com.vaadin.terminal.gwt.client.ui.TouchScrollDelegate.TouchScrollHandler;
+
+/**
+ * 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.tabsheet.VTabsheet}.
+ *
+ * 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 VTabsheetPanel extends ComplexPanel {
+
+ private Widget visibleWidget;
+
+ private final TouchScrollHandler touchScrollHandler;
+
+ /**
+ * Creates an empty tabsheet panel.
+ */
+ public VTabsheetPanel() {
+ setElement(DOM.createDiv());
+ touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
+ }
+
+ /**
+ * 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");
+ hide(el);
+ touchScrollHandler.addElement(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);
+ }
+ touchScrollHandler.removeElement(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;
+ touchScrollHandler.setElements(visibleWidget.getElement()
+ .getParentElement());
+ }
+ // Always ensure the selected tab is visible. If server prevents a tab
+ // change we might end up here with visibleWidget == newVisible but its
+ // parent is still hidden.
+ 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();
+ }
+ // v-tabsheet-tabsheetpanel height
+ getElement().getStyle().setPropertyPx("height", height);
+
+ // widget wrapper height
+ if (dynamicHeight) {
+ wrapperDiv.getStyle().clearHeight();
+ } else {
+ // widget wrapper height
+ wrapperDiv.getStyle().setPropertyPx("height", height);
+ }
+ }
+
+ 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/client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java
new file mode 100644
index 0000000000..cdf648d3ec
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/TextAreaConnector.java
@@ -0,0 +1,33 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.textarea;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.textarea.TextAreaState;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.textfield.TextFieldConnector;
+import com.vaadin.ui.TextArea;
+
+@Connect(TextArea.class)
+public class TextAreaConnector extends TextFieldConnector {
+
+ @Override
+ public TextAreaState getState() {
+ return (TextAreaState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ getWidget().setRows(getState().getRows());
+ getWidget().setWordwrap(getState().isWordwrap());
+ }
+
+ @Override
+ public VTextArea getWidget() {
+ return (VTextArea) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java
new file mode 100644
index 0000000000..c0f6deab53
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/textarea/VTextArea.java
@@ -0,0 +1,105 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.textarea;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.TextAreaElement;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+/**
+ * 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 Vaadin Ltd.
+ *
+ */
+public class VTextArea extends VTextField {
+ public static final String CLASSNAME = "v-textarea";
+ private boolean wordwrap = true;
+
+ public VTextArea() {
+ super(DOM.createTextArea());
+ setStyleName(CLASSNAME);
+ }
+
+ public TextAreaElement getTextAreaElement() {
+ return super.getElement().cast();
+ }
+
+ public void setRows(int rows) {
+ getTextAreaElement().setRows(rows);
+ }
+
+ @Override
+ protected void setMaxLength(int newMaxLength) {
+ super.setMaxLength(newMaxLength);
+
+ boolean hasMaxLength = (newMaxLength >= 0);
+
+ if (hasMaxLength) {
+ sinkEvents(Event.ONKEYUP);
+ } else {
+ unsinkEvents(Event.ONKEYUP);
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (getMaxLength() >= 0 && event.getTypeInt() == Event.ONKEYUP) {
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ if (getText().length() > getMaxLength()) {
+ setText(getText().substring(0, getMaxLength()));
+ }
+ }
+ });
+ }
+ super.onBrowserEvent(event);
+ }
+
+ @Override
+ public int getCursorPos() {
+ // This is needed so that TextBoxImplIE6 is used to return the correct
+ // position for old Internet Explorer versions where it has to be
+ // detected in a different way.
+ return getImpl().getTextAreaCursorPos(getElement());
+ }
+
+ @Override
+ protected void setMaxLengthToElement(int newMaxLength) {
+ // There is no maxlength property for textarea. The maximum length is
+ // enforced by the KEYUP handler
+
+ }
+
+ public void setWordwrap(boolean wordwrap) {
+ if (wordwrap == this.wordwrap) {
+ return; // No change
+ }
+
+ if (wordwrap) {
+ getElement().removeAttribute("wrap");
+ getElement().getStyle().clearOverflow();
+ } else {
+ getElement().setAttribute("wrap", "off");
+ getElement().getStyle().setOverflow(Overflow.AUTO);
+ }
+ if (BrowserInfo.get().isOpera()) {
+ // Opera fails to dynamically update the wrap attribute so we detach
+ // and reattach the whole TextArea.
+ Util.detachAttach(getElement());
+ }
+ this.wordwrap = wordwrap;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java
new file mode 100644
index 0000000000..d98d27942a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/TextFieldConnector.java
@@ -0,0 +1,106 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.textfield;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Event;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.textfield.AbstractTextFieldState;
+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.ui.AbstractFieldConnector;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener;
+import com.vaadin.ui.TextField;
+
+@Connect(value = TextField.class, loadStyle = LoadStyle.EAGER)
+public class TextFieldConnector extends AbstractFieldConnector implements
+ Paintable, BeforeShortcutActionListener {
+
+ @Override
+ public AbstractTextFieldState getState() {
+ return (AbstractTextFieldState) super.getState();
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Save details
+ getWidget().client = client;
+ getWidget().paintableId = uidl.getId();
+
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().setReadOnly(isReadOnly());
+
+ getWidget().setInputPrompt(getState().getInputPrompt());
+ getWidget().setMaxLength(getState().getMaxLength());
+ getWidget().setImmediate(getState().isImmediate());
+
+ getWidget().listenTextChangeEvents = hasEventListener("ie");
+ if (getWidget().listenTextChangeEvents) {
+ getWidget().textChangeEventMode = uidl
+ .getStringAttribute(VTextField.ATTR_TEXTCHANGE_EVENTMODE);
+ if (getWidget().textChangeEventMode
+ .equals(VTextField.TEXTCHANGE_MODE_EAGER)) {
+ getWidget().textChangeEventTimeout = 1;
+ } else {
+ getWidget().textChangeEventTimeout = uidl
+ .getIntAttribute(VTextField.ATTR_TEXTCHANGE_TIMEOUT);
+ if (getWidget().textChangeEventTimeout < 1) {
+ // Sanitize and allow lazy/timeout with timeout set to 0 to
+ // work as eager
+ getWidget().textChangeEventTimeout = 1;
+ }
+ }
+ getWidget().sinkEvents(VTextField.TEXTCHANGE_EVENTS);
+ getWidget().attachCutEventListener(getWidget().getElement());
+ }
+ getWidget().setColumns(getState().getColumns());
+
+ final String text = getState().getText();
+
+ /*
+ * We skip the text content update if field has been repainted, but text
+ * has not been changed. Additional sanity check verifies there is no
+ * change in the que (in which case we count more on the server side
+ * value).
+ */
+ if (!(uidl
+ .getBooleanAttribute(VTextField.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS)
+ && getWidget().valueBeforeEdit != null && text
+ .equals(getWidget().valueBeforeEdit))) {
+ getWidget().updateFieldContent(text);
+ }
+
+ if (uidl.hasAttribute("selpos")) {
+ final int pos = uidl.getIntAttribute("selpos");
+ final int length = uidl.getIntAttribute("sellen");
+ /*
+ * Gecko defers setting the text so we need to defer the selection.
+ */
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ getWidget().setSelectionRange(pos, length);
+ }
+ });
+ }
+ }
+
+ @Override
+ public VTextField getWidget() {
+ return (VTextField) super.getWidget();
+ }
+
+ @Override
+ public void onBeforeShortcutAction(Event e) {
+ getWidget().valueChange(false);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java
new file mode 100644
index 0000000000..7f8e549550
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/textfield/VTextField.java
@@ -0,0 +1,413 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.textfield;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+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.TextBoxBase;
+import com.vaadin.shared.EventId;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Field;
+
+/**
+ * This class represents a basic text input field with one row.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+public class VTextField extends TextBoxBase implements Field, ChangeHandler,
+ FocusHandler, BlurHandler, KeyDownHandler {
+
+ public static final String VAR_CUR_TEXT = "curText";
+
+ public static final String ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS = "nvc";
+ /**
+ * The input node CSS classname.
+ */
+ public static final String CLASSNAME = "v-textfield";
+ /**
+ * This CSS classname is added to the input node on hover.
+ */
+ public static final String CLASSNAME_FOCUS = "focus";
+
+ protected String paintableId;
+
+ protected ApplicationConnection client;
+
+ protected String valueBeforeEdit = null;
+
+ /**
+ * Set to false if a text change event has been sent since the last value
+ * change event. This means that {@link #valueBeforeEdit} should not be
+ * trusted when determining whether a text change even should be sent.
+ */
+ private boolean valueBeforeEditIsSynced = true;
+
+ private boolean immediate = false;
+ private int maxLength = -1;
+
+ private static final String CLASSNAME_PROMPT = "prompt";
+ public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet";
+ public static final String VAR_CURSOR = "c";
+ public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem";
+ protected static final String TEXTCHANGE_MODE_EAGER = "EAGER";
+ private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT";
+
+ private String inputPrompt = null;
+ private boolean prompting = false;
+ private int lastCursorPos = -1;
+
+ public VTextField() {
+ this(DOM.createInputText());
+ }
+
+ protected VTextField(Element node) {
+ super(node);
+ setStyleName(CLASSNAME);
+ addChangeHandler(this);
+ if (BrowserInfo.get().isIE()) {
+ // IE does not send change events when pressing enter in a text
+ // input so we handle it using a key listener instead
+ addKeyDownHandler(this);
+ }
+ addFocusHandler(this);
+ addBlurHandler(this);
+ }
+
+ /*
+ * TODO When GWT adds ONCUT, add it there and remove workaround. See
+ * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030
+ *
+ * Also note that the cut/paste are not totally crossbrowsers compatible.
+ * E.g. in Opera mac works via context menu, but on via File->Paste/Cut.
+ * Opera might need the polling method for 100% working textchanceevents.
+ * Eager polling for a change is bit dum and heavy operation, so I guess we
+ * should first try to survive without.
+ */
+ protected static final int TEXTCHANGE_EVENTS = Event.ONPASTE
+ | Event.KEYEVENTS | Event.ONMOUSEUP;
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ if (listenTextChangeEvents
+ && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event
+ .getTypeInt()) {
+ deferTextChangeEvent();
+ }
+
+ }
+
+ /*
+ * TODO optimize this so that only changes are sent + make the value change
+ * event just a flag that moves the current text to value
+ */
+ private String lastTextChangeString = null;
+
+ private String getLastCommunicatedString() {
+ return lastTextChangeString;
+ }
+
+ private void communicateTextValueToServer() {
+ String text = getText();
+ if (prompting) {
+ // Input prompt visible, text is actually ""
+ text = "";
+ }
+ if (!text.equals(getLastCommunicatedString())) {
+ if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) {
+ /*
+ * Value change for the current text has been enqueued since the
+ * last text change event was sent, but we can't know that it
+ * has been sent to the server. Ensure that all pending changes
+ * are sent now. Sending a value change without a text change
+ * will simulate a TextChangeEvent on the server.
+ */
+ client.sendPendingVariableChanges();
+ } else {
+ // Default case - just send an immediate text change message
+ client.updateVariable(paintableId, VAR_CUR_TEXT, text, true);
+
+ // Shouldn't investigate valueBeforeEdit to avoid duplicate text
+ // change events as the states are not in sync any more
+ valueBeforeEditIsSynced = false;
+ }
+ lastTextChangeString = text;
+ }
+ }
+
+ private Timer textChangeEventTrigger = new Timer() {
+
+ @Override
+ public void run() {
+ if (isAttached()) {
+ updateCursorPosition();
+ communicateTextValueToServer();
+ scheduled = false;
+ }
+ }
+ };
+ private boolean scheduled = false;
+ protected boolean listenTextChangeEvents;
+ protected String textChangeEventMode;
+ protected int textChangeEventTimeout;
+
+ private void deferTextChangeEvent() {
+ if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) {
+ return;
+ } else {
+ textChangeEventTrigger.cancel();
+ }
+ textChangeEventTrigger.schedule(getTextChangeEventTimeout());
+ scheduled = true;
+ }
+
+ private int getTextChangeEventTimeout() {
+ return textChangeEventTimeout;
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ boolean wasReadOnly = isReadOnly();
+
+ if (readOnly) {
+ setTabIndex(-1);
+ } else if (wasReadOnly && !readOnly && getTabIndex() == -1) {
+ /*
+ * Need to manually set tab index to 0 since server will not send
+ * the tab index if it is 0.
+ */
+ setTabIndex(0);
+ }
+
+ super.setReadOnly(readOnly);
+ }
+
+ protected void updateFieldContent(final String text) {
+ setPrompting(inputPrompt != null && focusedTextField != this
+ && (text.equals("")));
+
+ String fieldValue;
+ if (prompting) {
+ fieldValue = isReadOnly() ? "" : inputPrompt;
+ addStyleDependentName(CLASSNAME_PROMPT);
+ } else {
+ fieldValue = text;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ setText(fieldValue);
+
+ lastTextChangeString = valueBeforeEdit = text;
+ valueBeforeEditIsSynced = true;
+ }
+
+ protected void onCut() {
+ if (listenTextChangeEvents) {
+ deferTextChangeEvent();
+ }
+ }
+
+ protected native void attachCutEventListener(Element el)
+ /*-{
+ var me = this;
+ el.oncut = $entry(function() {
+ me.@com.vaadin.terminal.gwt.client.ui.textfield.VTextField::onCut()();
+ });
+ }-*/;
+
+ protected native void detachCutEventListener(Element el)
+ /*-{
+ el.oncut = null;
+ }-*/;
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ detachCutEventListener(getElement());
+ if (focusedTextField == this) {
+ focusedTextField = null;
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (listenTextChangeEvents) {
+ detachCutEventListener(getElement());
+ }
+ }
+
+ protected void setMaxLength(int newMaxLength) {
+ if (newMaxLength >= 0) {
+ maxLength = newMaxLength;
+ } else {
+ maxLength = -1;
+ }
+ setMaxLengthToElement(newMaxLength);
+ }
+
+ protected void setMaxLengthToElement(int newMaxLength) {
+ if (newMaxLength >= 0) {
+ getElement().setPropertyInt("maxLength", newMaxLength);
+ } else {
+ getElement().removeAttribute("maxLength");
+ }
+ }
+
+ public int getMaxLength() {
+ return maxLength;
+ }
+
+ @Override
+ public void onChange(ChangeEvent event) {
+ valueChange(false);
+ }
+
+ /**
+ * Called when the field value might have changed and/or the field was
+ * blurred. These are combined so the blur event is sent in the same batch
+ * as a possible value change event (these are often connected).
+ *
+ * @param blurred
+ * true if the field was blurred
+ */
+ public void valueChange(boolean blurred) {
+ if (client != null && paintableId != null) {
+ boolean sendBlurEvent = false;
+ boolean sendValueChange = false;
+
+ if (blurred && client.hasEventListeners(this, EventId.BLUR)) {
+ sendBlurEvent = true;
+ client.updateVariable(paintableId, EventId.BLUR, "", false);
+ }
+
+ String newText = getText();
+ if (!prompting && newText != null
+ && !newText.equals(valueBeforeEdit)) {
+ sendValueChange = immediate;
+ client.updateVariable(paintableId, "text", newText, false);
+ valueBeforeEdit = newText;
+ valueBeforeEditIsSynced = true;
+ }
+
+ /*
+ * also send cursor position, no public api yet but for easier
+ * extension
+ */
+ updateCursorPosition();
+
+ if (sendBlurEvent || sendValueChange) {
+ /*
+ * Avoid sending text change event as we will simulate it on the
+ * server side before value change events.
+ */
+ textChangeEventTrigger.cancel();
+ scheduled = false;
+ client.sendPendingVariableChanges();
+ }
+ }
+ }
+
+ /**
+ * Updates the cursor position variable if it has changed since the last
+ * update.
+ *
+ * @return true iff the value was updated
+ */
+ protected boolean updateCursorPosition() {
+ if (Util.isAttachedAndDisplayed(this)) {
+ int cursorPos = getCursorPos();
+ if (lastCursorPos != cursorPos) {
+ client.updateVariable(paintableId, VAR_CURSOR, cursorPos, false);
+ lastCursorPos = cursorPos;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static VTextField focusedTextField;
+
+ public static void flushChangesFromFocusedTextField() {
+ if (focusedTextField != null) {
+ focusedTextField.onChange(null);
+ }
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ addStyleDependentName(CLASSNAME_FOCUS);
+ if (prompting) {
+ setText("");
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ setPrompting(false);
+ }
+ focusedTextField = this;
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(paintableId, EventId.FOCUS, "", true);
+ }
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ // this is called twice on Chrome when e.g. changing tab while prompting
+ // field focused - do not change settings on the second time
+ if (focusedTextField != this) {
+ return;
+ }
+ removeStyleDependentName(CLASSNAME_FOCUS);
+ focusedTextField = null;
+ String text = getText();
+ setPrompting(inputPrompt != null && (text == null || "".equals(text)));
+ if (prompting) {
+ setText(isReadOnly() ? "" : inputPrompt);
+ addStyleDependentName(CLASSNAME_PROMPT);
+ }
+
+ valueChange(true);
+ }
+
+ private void setPrompting(boolean prompting) {
+ this.prompting = prompting;
+ }
+
+ public void setColumns(int columns) {
+ if (columns <= 0) {
+ return;
+ }
+
+ setWidth(columns + "em");
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+ valueChange(false);
+ }
+ }
+
+ public void setImmediate(boolean immediate) {
+ this.immediate = immediate;
+ }
+
+ public void setInputPrompt(String inputPrompt) {
+ this.inputPrompt = inputPrompt;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java
new file mode 100644
index 0000000000..def63edae9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tree/TreeConnector.java
@@ -0,0 +1,287 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.tree;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.google.gwt.dom.client.Element;
+import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.ui.Connect;
+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.TooltipInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.tree.VTree.TreeNode;
+import com.vaadin.ui.Tree;
+
+@Connect(Tree.class)
+public class TreeConnector extends AbstractComponentConnector implements
+ Paintable {
+
+ public static final String ATTRIBUTE_NODE_STYLE = "style";
+ public static final String ATTRIBUTE_NODE_CAPTION = "caption";
+ public static final String ATTRIBUTE_NODE_ICON = "icon";
+
+ public static final String ATTRIBUTE_ACTION_CAPTION = "caption";
+ public static final String ATTRIBUTE_ACTION_ICON = ATTRIBUTE_NODE_ICON;
+
+ protected final Map<TreeNode, TooltipInfo> tooltipMap = new HashMap<TreeNode, TooltipInfo>();
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().rendering = true;
+
+ getWidget().client = client;
+
+ if (uidl.hasAttribute("partialUpdate")) {
+ handleUpdate(uidl);
+ getWidget().rendering = false;
+ return;
+ }
+
+ getWidget().paintableId = uidl.getId();
+
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().disabled = !isEnabled();
+ getWidget().readonly = isReadOnly();
+
+ getWidget().dragMode = uidl.hasAttribute("dragMode") ? uidl
+ .getIntAttribute("dragMode") : 0;
+
+ getWidget().isNullSelectionAllowed = uidl
+ .getBooleanAttribute("nullselect");
+
+ if (uidl.hasAttribute("alb")) {
+ getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb");
+ }
+
+ getWidget().body.clear();
+ // clear out any references to nodes that no longer are attached
+ getWidget().clearNodeToKeyMap();
+ tooltipMap.clear();
+
+ TreeNode childTree = null;
+ UIDL childUidl = null;
+ for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) {
+ childUidl = (UIDL) i.next();
+ if ("actions".equals(childUidl.getTag())) {
+ updateActionMap(childUidl);
+ continue;
+ } else if ("-ac".equals(childUidl.getTag())) {
+ getWidget().updateDropHandler(childUidl);
+ continue;
+ }
+ childTree = getWidget().new TreeNode();
+ getConnection().getVTooltip().connectHandlersToWidget(childTree);
+ updateNodeFromUIDL(childTree, childUidl);
+ getWidget().body.add(childTree);
+ childTree.addStyleDependentName("root");
+ childTree.childNodeContainer.addStyleDependentName("root");
+ }
+ if (childTree != null && childUidl != null) {
+ boolean leaf = !childUidl.getTag().equals("node");
+ childTree.addStyleDependentName(leaf ? "leaf-last" : "last");
+ childTree.childNodeContainer.addStyleDependentName("last");
+ }
+ final String selectMode = uidl.getStringAttribute("selectmode");
+ getWidget().selectable = !"none".equals(selectMode);
+ getWidget().isMultiselect = "multi".equals(selectMode);
+
+ if (getWidget().isMultiselect) {
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Always use the simple mode for touch devices that do not have
+ // shift/ctrl keys (#8595)
+ getWidget().multiSelectMode = VTree.MULTISELECT_MODE_SIMPLE;
+ } else {
+ getWidget().multiSelectMode = uidl
+ .getIntAttribute("multiselectmode");
+ }
+ }
+
+ getWidget().selectedIds = uidl.getStringArrayVariableAsSet("selected");
+
+ // Update lastSelection and focusedNode to point to *actual* nodes again
+ // after the old ones have been cleared from the body. This fixes focus
+ // and keyboard navigation issues as described in #7057 and other
+ // tickets.
+ if (getWidget().lastSelection != null) {
+ getWidget().lastSelection = getWidget().getNodeByKey(
+ getWidget().lastSelection.key);
+ }
+ if (getWidget().focusedNode != null) {
+ getWidget().setFocusedNode(
+ getWidget().getNodeByKey(getWidget().focusedNode.key));
+ }
+
+ if (getWidget().lastSelection == null
+ && getWidget().focusedNode == null
+ && !getWidget().selectedIds.isEmpty()) {
+ getWidget().setFocusedNode(
+ getWidget().getNodeByKey(
+ getWidget().selectedIds.iterator().next()));
+ getWidget().focusedNode.setFocused(false);
+ }
+
+ getWidget().rendering = false;
+
+ }
+
+ @Override
+ public VTree getWidget() {
+ return (VTree) super.getWidget();
+ }
+
+ private void handleUpdate(UIDL uidl) {
+ final TreeNode rootNode = getWidget().getNodeByKey(
+ uidl.getStringAttribute("rootKey"));
+ if (rootNode != null) {
+ if (!rootNode.getState()) {
+ // expanding node happened server side
+ rootNode.setState(true, false);
+ }
+ renderChildNodes(rootNode, (Iterator) uidl.getChildIterator());
+ }
+ }
+
+ /**
+ * Registers action for the root and also for individual nodes
+ *
+ * @param uidl
+ */
+ private void updateActionMap(UIDL uidl) {
+ final Iterator<?> it = uidl.getChildIterator();
+ while (it.hasNext()) {
+ final UIDL action = (UIDL) it.next();
+ final String key = action.getStringAttribute("key");
+ final String caption = action
+ .getStringAttribute(ATTRIBUTE_ACTION_CAPTION);
+ String iconUrl = null;
+ if (action.hasAttribute(ATTRIBUTE_ACTION_ICON)) {
+ iconUrl = getConnection().translateVaadinUri(
+ action.getStringAttribute(ATTRIBUTE_ACTION_ICON));
+ }
+ getWidget().registerAction(key, caption, iconUrl);
+ }
+
+ }
+
+ public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl) {
+ String nodeKey = uidl.getStringAttribute("key");
+ treeNode.setText(uidl.getStringAttribute(ATTRIBUTE_NODE_CAPTION));
+ treeNode.key = nodeKey;
+
+ getWidget().registerNode(treeNode);
+
+ if (uidl.hasAttribute("al")) {
+ treeNode.actionKeys = uidl.getStringArrayAttribute("al");
+ }
+
+ if (uidl.getTag().equals("node")) {
+ if (uidl.getChildCount() == 0) {
+ treeNode.childNodeContainer.setVisible(false);
+ } else {
+ renderChildNodes(treeNode, (Iterator) uidl.getChildIterator());
+ treeNode.childrenLoaded = true;
+ }
+ } else {
+ treeNode.addStyleName(TreeNode.CLASSNAME + "-leaf");
+ }
+ if (uidl.hasAttribute(ATTRIBUTE_NODE_STYLE)) {
+ treeNode.setNodeStyleName(uidl
+ .getStringAttribute(ATTRIBUTE_NODE_STYLE));
+ }
+
+ String description = uidl.getStringAttribute("descr");
+ if (description != null) {
+ tooltipMap.put(treeNode, new TooltipInfo(description));
+ }
+
+ if (uidl.getBooleanAttribute("expanded") && !treeNode.getState()) {
+ treeNode.setState(true, false);
+ }
+
+ if (uidl.getBooleanAttribute("selected")) {
+ treeNode.setSelected(true);
+ // ensure that identifier is in selectedIds array (this may be a
+ // partial update)
+ getWidget().selectedIds.add(nodeKey);
+ }
+
+ treeNode.setIcon(uidl.getStringAttribute(ATTRIBUTE_NODE_ICON));
+ }
+
+ void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i) {
+ containerNode.childNodeContainer.clear();
+ containerNode.childNodeContainer.setVisible(true);
+ while (i.hasNext()) {
+ final UIDL childUidl = 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 = getWidget().new TreeNode();
+ getConnection().getVTooltip().connectHandlersToWidget(childTree);
+ updateNodeFromUIDL(childTree, childUidl);
+ containerNode.childNodeContainer.add(childTree);
+ if (!i.hasNext()) {
+ childTree
+ .addStyleDependentName(childTree.isLeaf() ? "leaf-last"
+ : "last");
+ childTree.childNodeContainer.addStyleDependentName("last");
+ }
+ }
+ containerNode.childrenLoaded = true;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().isPropertyReadOnly();
+ }
+
+ @Override
+ public AbstractFieldState getState() {
+ return (AbstractFieldState) super.getState();
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+
+ TooltipInfo info = null;
+
+ // Try to find a tooltip for a node
+ if (element != getWidget().getElement()) {
+ Object node = Util.findWidget(
+ (com.google.gwt.user.client.Element) element,
+ TreeNode.class);
+
+ if (node != null) {
+ TreeNode tnode = (TreeNode) node;
+ if (tnode.isCaptionElement(element)) {
+ info = tooltipMap.get(tnode);
+ }
+ }
+ }
+
+ // If no tooltip found for the node or if the target was not a node, use
+ // the default tooltip
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java b/client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java
new file mode 100644
index 0000000000..f5fe6bce1a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java
@@ -0,0 +1,2128 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.tree;
+
+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.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+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.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Action;
+import com.vaadin.terminal.gwt.client.ui.ActionOwner;
+import com.vaadin.terminal.gwt.client.ui.FocusElementPanel;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.TreeAction;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
+import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
+
+/**
+ *
+ */
+public class VTree extends FocusElementPanel implements VHasDropHandler,
+ FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler,
+ SubPartAware, ActionOwner {
+
+ public static final String CLASSNAME = "v-tree";
+
+ public static final String ITEM_CLICK_EVENT_ID = "itemClick";
+
+ /**
+ * Click selects the current node, ctrl/shift toggles multi selection
+ */
+ public static final int MULTISELECT_MODE_DEFAULT = 0;
+
+ /**
+ * Click/touch on node toggles its selected status
+ */
+ public static final int MULTISELECT_MODE_SIMPLE = 1;
+
+ private static final int CHARCODE_SPACE = 32;
+
+ final FlowPanel body = new FlowPanel();
+
+ Set<String> selectedIds = new HashSet<String>();
+ ApplicationConnection client;
+ String paintableId;
+ boolean selectable;
+ boolean isMultiselect;
+ private String currentMouseOverKey;
+ TreeNode lastSelection;
+ TreeNode focusedNode;
+ int multiSelectMode = MULTISELECT_MODE_DEFAULT;
+
+ 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>();
+
+ boolean immediate;
+
+ boolean isNullSelectionAllowed = true;
+
+ boolean disabled = false;
+
+ boolean readonly;
+
+ boolean rendering;
+
+ private VAbstractDropHandler dropHandler;
+
+ int dragMode;
+
+ private boolean selectionHasChanged = false;
+
+ String[] bodyActionKeys;
+
+ public VLazyExecutor iconLoaded = new VLazyExecutor(50,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ Util.notifyParentOfSizeChange(VTree.this, true);
+ }
+
+ });
+
+ public VTree() {
+ super();
+ setStyleName(CLASSNAME);
+ add(body);
+
+ addFocusHandler(this);
+ addBlurHandler(this);
+
+ /*
+ * Listen to context menu events on the empty space in the tree
+ */
+ sinkEvents(Event.ONCONTEXTMENU);
+ addDomHandler(new ContextMenuHandler() {
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ handleBodyContextMenu(event);
+ }
+ }, ContextMenuEvent.getType());
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko() || BrowserInfo.get().isOpera()) {
+ addKeyPressHandler(this);
+ } else {
+ addKeyDownHandler(this);
+ }
+
+ /*
+ * We need to use the sinkEvents method to catch the keyUp events so we
+ * can cache a single shift. KeyUpHandler cannot do this. At the same
+ * time we catch the mouse down and up events so we can apply the text
+ * selection patch in IE
+ */
+ sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP);
+
+ /*
+ * Re-set the tab index to make sure that the FocusElementPanel's
+ * (super) focus element gets the tab index and not the element
+ * containing the tree.
+ */
+ setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
+ * .client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+ // Prevent default text selection in IE
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast()).setPropertyJSO(
+ "onselectstart", applyDisableTextSelectionIEHack());
+ }
+ } else if (event.getTypeInt() == Event.ONMOUSEUP) {
+ // Remove IE text selection hack
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast()).setPropertyJSO(
+ "onselectstart", null);
+ }
+ } else if (event.getTypeInt() == Event.ONKEYUP) {
+ if (selectionHasChanged) {
+ if (event.getKeyCode() == getNavigationDownKey()
+ && !event.getShiftKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == getNavigationUpKey()
+ && !event.getShiftKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == getNavigationSelectKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ }
+ }
+ }
+ }
+
+ public String getActionCaption(String actionKey) {
+ return actionMap.get(actionKey + "_c");
+ }
+
+ public String getActionIcon(String actionKey) {
+ return actionMap.get(actionKey + "_i");
+ }
+
+ /**
+ * Returns the first root node of the tree or null if there are no root
+ * nodes.
+ *
+ * @return The first root {@link TreeNode}
+ */
+ protected TreeNode getFirstRootNode() {
+ if (body.getWidgetCount() == 0) {
+ return null;
+ }
+ return (TreeNode) body.getWidget(0);
+ }
+
+ /**
+ * Returns the last root node of the tree or null if there are no root
+ * nodes.
+ *
+ * @return The last root {@link TreeNode}
+ */
+ protected TreeNode getLastRootNode() {
+ if (body.getWidgetCount() == 0) {
+ return null;
+ }
+ return (TreeNode) body.getWidget(body.getWidgetCount() - 1);
+ }
+
+ /**
+ * Returns a list of all root nodes in the Tree in the order they appear in
+ * the tree.
+ *
+ * @return A list of all root {@link TreeNode}s.
+ */
+ protected List<TreeNode> getRootNodes() {
+ ArrayList<TreeNode> rootNodes = new ArrayList<TreeNode>();
+ for (int i = 0; i < body.getWidgetCount(); i++) {
+ rootNodes.add((TreeNode) body.getWidget(i));
+ }
+ return rootNodes;
+ }
+
+ private void updateTreeRelatedDragData(VDragEvent drag) {
+
+ currentMouseOverKey = findCurrentMouseOverKey(drag.getElementOver());
+
+ drag.getDropDetails().put("itemIdOver", currentMouseOverKey);
+ if (currentMouseOverKey != null) {
+ TreeNode treeNode = getNodeByKey(currentMouseOverKey);
+ VerticalDropLocation detail = treeNode.getDropDetail(drag
+ .getCurrentGwtEvent());
+ Boolean overTreeNode = null;
+ if (treeNode != null && !treeNode.isLeaf()
+ && detail == VerticalDropLocation.MIDDLE) {
+ overTreeNode = true;
+ }
+ drag.getDropDetails().put("itemIdOverIsNode", overTreeNode);
+ drag.getDropDetails().put("detail", detail);
+ } else {
+ drag.getDropDetails().put("itemIdOverIsNode", null);
+ drag.getDropDetails().put("detail", null);
+ }
+
+ }
+
+ private String findCurrentMouseOverKey(Element elementOver) {
+ TreeNode treeNode = Util.findWidget(elementOver, TreeNode.class);
+ return treeNode == null ? null : treeNode.key;
+ }
+
+ void updateDropHandler(UIDL childUidl) {
+ if (dropHandler == null) {
+ dropHandler = new VAbstractDropHandler() {
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ }
+
+ @Override
+ protected void dragAccepted(final VDragEvent drag) {
+
+ }
+
+ @Override
+ public void dragOver(final VDragEvent currentDrag) {
+ final Object oldIdOver = currentDrag.getDropDetails().get(
+ "itemIdOver");
+ final VerticalDropLocation oldDetail = (VerticalDropLocation) currentDrag
+ .getDropDetails().get("detail");
+
+ updateTreeRelatedDragData(currentDrag);
+ final VerticalDropLocation detail = (VerticalDropLocation) currentDrag
+ .getDropDetails().get("detail");
+ boolean nodeHasChanged = (currentMouseOverKey != null && currentMouseOverKey != oldIdOver)
+ || (currentMouseOverKey == null && oldIdOver != null);
+ boolean detailHasChanded = (detail != null && detail != oldDetail)
+ || (detail == null && oldDetail != null);
+
+ if (nodeHasChanged || detailHasChanded) {
+ final String newKey = currentMouseOverKey;
+ TreeNode treeNode = keyToNode.get(oldIdOver);
+ if (treeNode != null) {
+ // clear old styles
+ treeNode.emphasis(null);
+ }
+ if (newKey != null) {
+ validate(new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ VerticalDropLocation curDetail = (VerticalDropLocation) event
+ .getDropDetails().get("detail");
+ if (curDetail == detail
+ && newKey.equals(currentMouseOverKey)) {
+ getNodeByKey(newKey).emphasis(detail);
+ }
+ /*
+ * Else drag is already on a different
+ * node-detail pair, new criteria check is
+ * going on
+ */
+ }
+ }, currentDrag);
+
+ }
+ }
+
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ cleanUp();
+ }
+
+ private void cleanUp() {
+ if (currentMouseOverKey != null) {
+ getNodeByKey(currentMouseOverKey).emphasis(null);
+ currentMouseOverKey = null;
+ }
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ cleanUp();
+ return super.drop(drag);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(VTree.this);
+ }
+
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ };
+ }
+ dropHandler.updateAcceptRules(childUidl);
+ }
+
+ public void setSelected(TreeNode treeNode, boolean selected) {
+ if (selected) {
+ if (!isMultiselect) {
+ while (selectedIds.size() > 0) {
+ final String id = selectedIds.iterator().next();
+ final TreeNode oldSelection = getNodeByKey(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);
+ }
+
+ sendSelectionToServer();
+ }
+
+ /**
+ * Sends the selection to the server
+ */
+ private void sendSelectionToServer() {
+ Command command = new Command() {
+ @Override
+ public void execute() {
+ client.updateVariable(paintableId, "selected",
+ selectedIds.toArray(new String[selectedIds.size()]),
+ immediate);
+ selectionHasChanged = false;
+ }
+ };
+
+ /*
+ * Delaying the sending of the selection in webkit to ensure the
+ * selection is always sent when the tree has focus and after click
+ * events have been processed. This is due to the focusing
+ * implementation in FocusImplSafari which uses timeouts when focusing
+ * and blurring.
+ */
+ if (BrowserInfo.get().isWebkit()) {
+ Scheduler.get().scheduleDeferred(command);
+ } else {
+ command.execute();
+ }
+ }
+
+ /**
+ * Is a node selected in the tree
+ *
+ * @param treeNode
+ * The node to check
+ * @return
+ */
+ public boolean isSelected(TreeNode treeNode) {
+ return selectedIds.contains(treeNode.key);
+ }
+
+ public class TreeNode extends SimplePanel implements ActionOwner {
+
+ public static final String CLASSNAME = "v-tree-node";
+ public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused";
+
+ public String key;
+
+ String[] actionKeys = null;
+
+ boolean childrenLoaded;
+
+ Element nodeCaptionDiv;
+
+ protected Element nodeCaptionSpan;
+
+ FlowPanel childNodeContainer;
+
+ private boolean open;
+
+ private Icon icon;
+
+ private Event mouseDownEvent;
+
+ private int cachedHeight = -1;
+
+ private boolean focused = false;
+
+ public TreeNode() {
+ constructDom();
+ sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
+ | Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
+ }
+
+ public VerticalDropLocation getDropDetail(NativeEvent currentGwtEvent) {
+ if (cachedHeight < 0) {
+ /*
+ * Height is cached to avoid flickering (drop hints may change
+ * the reported offsetheight -> would change the drop detail)
+ */
+ cachedHeight = nodeCaptionDiv.getOffsetHeight();
+ }
+ VerticalDropLocation verticalDropLocation = DDUtil
+ .getVerticalDropLocation(nodeCaptionDiv, cachedHeight,
+ currentGwtEvent, 0.15);
+ return verticalDropLocation;
+ }
+
+ protected void emphasis(VerticalDropLocation detail) {
+ String base = "v-tree-node-drag-";
+ UIObject.setStyleName(getElement(), base + "top",
+ VerticalDropLocation.TOP == detail);
+ UIObject.setStyleName(getElement(), base + "bottom",
+ VerticalDropLocation.BOTTOM == detail);
+ UIObject.setStyleName(getElement(), base + "center",
+ VerticalDropLocation.MIDDLE == detail);
+ base = "v-tree-node-caption-drag-";
+ UIObject.setStyleName(nodeCaptionDiv, base + "top",
+ VerticalDropLocation.TOP == detail);
+ UIObject.setStyleName(nodeCaptionDiv, base + "bottom",
+ VerticalDropLocation.BOTTOM == detail);
+ UIObject.setStyleName(nodeCaptionDiv, base + "center",
+ VerticalDropLocation.MIDDLE == detail);
+
+ // also add classname to "folder node" into which the drag is
+ // targeted
+
+ TreeNode folder = null;
+ /* Possible parent of this TreeNode will be stored here */
+ TreeNode parentFolder = getParentNode();
+
+ // TODO fix my bugs
+ if (isLeaf()) {
+ folder = parentFolder;
+ // note, parent folder may be null if this is root node => no
+ // folder target exists
+ } else {
+ if (detail == VerticalDropLocation.TOP) {
+ folder = parentFolder;
+ } else {
+ folder = this;
+ }
+ // ensure we remove the dragfolder classname from the previous
+ // folder node
+ setDragFolderStyleName(this, false);
+ setDragFolderStyleName(parentFolder, false);
+ }
+ if (folder != null) {
+ setDragFolderStyleName(folder, detail != null);
+ }
+
+ }
+
+ private TreeNode getParentNode() {
+ Widget parent2 = getParent().getParent();
+ if (parent2 instanceof TreeNode) {
+ return (TreeNode) parent2;
+ }
+ return null;
+ }
+
+ private void setDragFolderStyleName(TreeNode folder, boolean add) {
+ if (folder != null) {
+ UIObject.setStyleName(folder.getElement(),
+ "v-tree-node-dragfolder", add);
+ UIObject.setStyleName(folder.nodeCaptionDiv,
+ "v-tree-node-caption-dragfolder", add);
+ }
+ }
+
+ /**
+ * Handles mouse selection
+ *
+ * @param ctrl
+ * Was the ctrl-key pressed
+ * @param shift
+ * Was the shift-key pressed
+ * @return Returns true if event was handled, else false
+ */
+ private boolean handleClickSelection(final boolean ctrl,
+ final boolean shift) {
+
+ // always when clicking an item, focus it
+ setFocusedNode(this, false);
+
+ if (!BrowserInfo.get().isOpera()) {
+ /*
+ * Ensure that the tree's focus element also gains focus
+ * (TreeNodes focus is faked using FocusElementPanel in browsers
+ * other than Opera).
+ */
+ focus();
+ }
+
+ ScheduledCommand command = new ScheduledCommand() {
+ @Override
+ public void execute() {
+
+ if (multiSelectMode == MULTISELECT_MODE_SIMPLE
+ || !isMultiselect) {
+ toggleSelection();
+ lastSelection = TreeNode.this;
+ } else if (multiSelectMode == MULTISELECT_MODE_DEFAULT) {
+ // Handle ctrl+click
+ if (isMultiselect && ctrl && !shift) {
+ toggleSelection();
+ lastSelection = TreeNode.this;
+
+ // Handle shift+click
+ } else if (isMultiselect && !ctrl && shift) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, key);
+ sendSelectionToServer();
+
+ // Handle ctrl+shift click
+ } else if (isMultiselect && ctrl && shift) {
+ selectNodeRange(lastSelection.key, key);
+
+ // Handle click
+ } else {
+ // TODO should happen only if this alone not yet
+ // selected,
+ // now sending excess server calls
+ deselectAll();
+ toggleSelection();
+ lastSelection = TreeNode.this;
+ }
+ }
+ }
+ };
+
+ if (BrowserInfo.get().isWebkit() && !treeHasFocus) {
+ /*
+ * Safari may need to wait for focus. See FocusImplSafari.
+ */
+ // VConsole.log("Deferring click handling to let webkit gain focus...");
+ Scheduler.get().scheduleDeferred(command);
+ } else {
+ command.execute();
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ final int type = DOM.eventGetType(event);
+ final Element target = DOM.eventGetTarget(event);
+
+ if (type == Event.ONLOAD && target == icon.getElement()) {
+ iconLoaded.trigger();
+ }
+
+ if (disabled) {
+ return;
+ }
+
+ final boolean inCaption = isCaptionElement(target);
+ if (inCaption
+ && client
+ .hasEventListeners(VTree.this, ITEM_CLICK_EVENT_ID)
+
+ && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) {
+ fireClick(event);
+ }
+ if (type == Event.ONCLICK) {
+ if (getElement() == target) {
+ // state change
+ toggleState();
+ } else if (!readonly && inCaption) {
+ if (selectable) {
+ // caption click = selection change && possible click
+ // event
+ if (handleClickSelection(
+ event.getCtrlKey() || event.getMetaKey(),
+ event.getShiftKey())) {
+ event.preventDefault();
+ }
+ } else {
+ // Not selectable, only focus the node.
+ setFocusedNode(this);
+ }
+ }
+ event.stopPropagation();
+ } else if (type == Event.ONCONTEXTMENU) {
+ showContextMenu(event);
+ }
+
+ if (dragMode != 0 || dropHandler != null) {
+ if (type == Event.ONMOUSEDOWN || type == Event.ONTOUCHSTART) {
+ if (nodeCaptionDiv.isOrHasChild((Node) event
+ .getEventTarget().cast())) {
+ if (dragMode > 0
+ && (type == Event.ONTOUCHSTART || event
+ .getButton() == NativeEvent.BUTTON_LEFT)) {
+ mouseDownEvent = event; // save event for possible
+ // dd operation
+ if (type == Event.ONMOUSEDOWN) {
+ event.preventDefault(); // prevent text
+ // selection
+ } else {
+ /*
+ * FIXME We prevent touch start event to be used
+ * as a scroll start event. Note that we cannot
+ * easily distinguish whether the user wants to
+ * drag or scroll. The same issue is in table
+ * that has scrollable area and has drag and
+ * drop enable. Some kind of timer might be used
+ * to resolve the issue.
+ */
+ event.stopPropagation();
+ }
+ }
+ }
+ } else if (type == Event.ONMOUSEMOVE
+ || type == Event.ONMOUSEOUT
+ || type == Event.ONTOUCHMOVE) {
+
+ if (mouseDownEvent != null) {
+ // start actual drag on slight move when mouse is down
+ VTransferable t = new VTransferable();
+ t.setDragSource(ConnectorMap.get(client).getConnector(
+ VTree.this));
+ t.setData("itemId", key);
+ VDragEvent drag = VDragAndDropManager.get().startDrag(
+ t, mouseDownEvent, true);
+
+ drag.createDragImage(nodeCaptionDiv, true);
+ event.stopPropagation();
+
+ mouseDownEvent = null;
+ }
+ } else if (type == Event.ONMOUSEUP) {
+ mouseDownEvent = null;
+ }
+ if (type == Event.ONMOUSEOVER) {
+ mouseDownEvent = null;
+ currentMouseOverKey = key;
+ event.stopPropagation();
+ }
+
+ } else if (type == Event.ONMOUSEDOWN
+ && event.getButton() == NativeEvent.BUTTON_LEFT) {
+ event.preventDefault(); // text selection
+ }
+ }
+
+ /**
+ * Checks if the given element is the caption or the icon.
+ *
+ * @param target
+ * The element to check
+ * @return true if the element is the caption or the icon
+ */
+ public boolean isCaptionElement(com.google.gwt.dom.client.Element target) {
+ return (target == nodeCaptionSpan || (icon != null && target == icon
+ .getElement()));
+ }
+
+ private void fireClick(final Event evt) {
+ /*
+ * Ensure we have focus in tree before sending variables. Otherwise
+ * previously modified field may contain dirty variables.
+ */
+ if (!treeHasFocus) {
+ if (BrowserInfo.get().isOpera()) {
+ if (focusedNode == null) {
+ getNodeByKey(key).setFocused(true);
+ } else {
+ focusedNode.setFocused(true);
+ }
+ } else {
+ focus();
+ }
+ }
+ final MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(evt);
+ ScheduledCommand command = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ // Determine if we should send the event immediately to the
+ // server. We do not want to send the event if there is a
+ // selection event happening after this. In all other cases
+ // we want to send it immediately.
+ boolean sendClickEventNow = true;
+
+ if (details.getButton() == NativeEvent.BUTTON_LEFT
+ && immediate && selectable) {
+ // Probably a selection that will cause a value change
+ // event to be sent
+ sendClickEventNow = false;
+
+ // The exception is that user clicked on the
+ // currently selected row and null selection is not
+ // allowed == no selection event
+ if (isSelected() && selectedIds.size() == 1
+ && !isNullSelectionAllowed) {
+ sendClickEventNow = true;
+ }
+ }
+
+ client.updateVariable(paintableId, "clickedKey", key, false);
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), sendClickEventNow);
+ }
+ };
+ if (treeHasFocus) {
+ command.execute();
+ } else {
+ /*
+ * Webkits need a deferring due to FocusImplSafari uses timeout
+ */
+ Scheduler.get().scheduleDeferred(command);
+ }
+ }
+
+ private void toggleSelection() {
+ if (selectable) {
+ VTree.this.setSelected(this, !isSelected());
+ }
+ }
+
+ private void toggleState() {
+ setState(!getState(), true);
+ }
+
+ protected void constructDom() {
+ addStyleName(CLASSNAME);
+
+ 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);
+
+ if (BrowserInfo.get().isOpera()) {
+ /*
+ * Focus the caption div of the node to get keyboard navigation
+ * to work without scrolling up or down when focusing a node.
+ */
+ nodeCaptionDiv.setTabIndex(-1);
+ }
+
+ childNodeContainer = new FlowPanel();
+ childNodeContainer.setStyleName(CLASSNAME + "-children");
+ setWidget(childNodeContainer);
+ }
+
+ public boolean isLeaf() {
+ String[] styleNames = getStyleName().split(" ");
+ for (String styleName : styleNames) {
+ if (styleName.equals(CLASSNAME + "-leaf")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ 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(VTree.this, false);
+ }
+ }
+
+ boolean getState() {
+ return open;
+ }
+
+ void setText(String text) {
+ DOM.setInnerText(nodeCaptionSpan, text);
+ }
+
+ public boolean isChildrenLoaded() {
+ return childrenLoaded;
+ }
+
+ /**
+ * Returns the children of the node
+ *
+ * @return A set of tree nodes
+ */
+ public List<TreeNode> getChildren() {
+ List<TreeNode> nodes = new LinkedList<TreeNode>();
+
+ if (!isLeaf() && isChildrenLoaded()) {
+ Iterator<Widget> iter = childNodeContainer.iterator();
+ while (iter.hasNext()) {
+ TreeNode node = (TreeNode) iter.next();
+ nodes.add(node);
+ }
+ }
+ return nodes;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /**
+ * Adds/removes Vaadin specific style name. This method ought to be
+ * called only from VTree.
+ *
+ * @param selected
+ */
+ protected void setSelected(boolean selected) {
+ // add style name to caption dom structure only, not to subtree
+ setStyleName(nodeCaptionDiv, "v-tree-node-selected", selected);
+ }
+
+ protected boolean isSelected() {
+ return VTree.this.isSelected(this);
+ }
+
+ /**
+ * Travels up the hierarchy looking for this node
+ *
+ * @param child
+ * The child which grandparent this is or is not
+ * @return True if this is a grandparent of the child node
+ */
+ public boolean isGrandParentOf(TreeNode child) {
+ TreeNode currentNode = child;
+ boolean isGrandParent = false;
+ while (currentNode != null) {
+ currentNode = currentNode.getParentNode();
+ if (currentNode == this) {
+ isGrandParent = true;
+ break;
+ }
+ }
+ return isGrandParent;
+ }
+
+ public boolean isSibling(TreeNode node) {
+ return node.getParentNode() == getParentNode();
+ }
+
+ 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.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.Widget#onDetach()
+ */
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ client.getContextMenu().ensureHidden(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.UIObject#toString()
+ */
+ @Override
+ public String toString() {
+ return nodeCaptionSpan.getInnerText();
+ }
+
+ /**
+ * Is the node focused?
+ *
+ * @param focused
+ * True if focused, false if not
+ */
+ public void setFocused(boolean focused) {
+ if (!this.focused && focused) {
+ nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED);
+
+ this.focused = focused;
+ if (BrowserInfo.get().isOpera()) {
+ nodeCaptionDiv.focus();
+ }
+ treeHasFocus = true;
+ } else if (this.focused && !focused) {
+ nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED);
+ this.focused = focused;
+ treeHasFocus = false;
+ }
+ }
+
+ /**
+ * Scrolls the caption into view
+ */
+ public void scrollIntoView() {
+ Util.scrollIntoViewVertically(nodeCaptionDiv);
+ }
+
+ public void setIcon(String iconUrl) {
+ if (iconUrl != null) {
+ // Add icon if not present
+ if (icon == null) {
+ icon = new Icon(client);
+ DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv),
+ icon.getElement(), nodeCaptionSpan);
+ }
+ icon.setUri(iconUrl);
+ } else {
+ // Remove icon if present
+ if (icon != null) {
+ DOM.removeChild(DOM.getFirstChild(nodeCaptionDiv),
+ icon.getElement());
+ icon = null;
+ }
+ }
+ }
+
+ public void setNodeStyleName(String styleName) {
+ addStyleName(TreeNode.CLASSNAME + "-" + styleName);
+ setStyleName(nodeCaptionDiv, TreeNode.CLASSNAME + "-caption-"
+ + styleName, true);
+ childNodeContainer.addStyleName(TreeNode.CLASSNAME + "-children-"
+ + styleName);
+
+ }
+
+ }
+
+ @Override
+ public VDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public TreeNode getNodeByKey(String key) {
+ return keyToNode.get(key);
+ }
+
+ /**
+ * Deselects all items in the tree
+ */
+ public void deselectAll() {
+ for (String key : selectedIds) {
+ TreeNode node = keyToNode.get(key);
+ if (node != null) {
+ node.setSelected(false);
+ }
+ }
+ selectedIds.clear();
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects a range of nodes
+ *
+ * @param startNodeKey
+ * The start node key
+ * @param endNodeKey
+ * The end node key
+ */
+ private void selectNodeRange(String startNodeKey, String endNodeKey) {
+
+ TreeNode startNode = keyToNode.get(startNodeKey);
+ TreeNode endNode = keyToNode.get(endNodeKey);
+
+ // The nodes have the same parent
+ if (startNode.getParent() == endNode.getParent()) {
+ doSiblingSelection(startNode, endNode);
+
+ // The start node is a grandparent of the end node
+ } else if (startNode.isGrandParentOf(endNode)) {
+ doRelationSelection(startNode, endNode);
+
+ // The end node is a grandparent of the start node
+ } else if (endNode.isGrandParentOf(startNode)) {
+ doRelationSelection(endNode, startNode);
+
+ } else {
+ doNoRelationSelection(startNode, endNode);
+ }
+ }
+
+ /**
+ * Selects a node and deselect all other nodes
+ *
+ * @param node
+ * The node to select
+ */
+ private void selectNode(TreeNode node, boolean deselectPrevious) {
+ if (deselectPrevious) {
+ deselectAll();
+ }
+
+ if (node != null) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ lastSelection = node;
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Deselects a node
+ *
+ * @param node
+ * The node to deselect
+ */
+ private void deselectNode(TreeNode node) {
+ node.setSelected(false);
+ selectedIds.remove(node.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects all the open children to a node
+ *
+ * @param node
+ * The parent node
+ */
+ private void selectAllChildren(TreeNode node, boolean includeRootNode) {
+ if (includeRootNode) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ }
+
+ for (TreeNode child : node.getChildren()) {
+ if (!child.isLeaf() && child.getState()) {
+ selectAllChildren(child, true);
+ } else {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects all children until a stop child is reached
+ *
+ * @param root
+ * The root not to start from
+ * @param stopNode
+ * The node to finish with
+ * @param includeRootNode
+ * Should the root node be selected
+ * @param includeStopNode
+ * Should the stop node be selected
+ *
+ * @return Returns false if the stop child was found, else true if all
+ * children was selected
+ */
+ private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode,
+ boolean includeRootNode, boolean includeStopNode) {
+ if (includeRootNode) {
+ root.setSelected(true);
+ selectedIds.add(root.key);
+ }
+ if (root.getState() && root != stopNode) {
+ for (TreeNode child : root.getChildren()) {
+ if (!child.isLeaf() && child.getState() && child != stopNode) {
+ if (!selectAllChildrenUntil(child, stopNode, true,
+ includeStopNode)) {
+ return false;
+ }
+ } else if (child == stopNode) {
+ if (includeStopNode) {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ return false;
+ } else {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ }
+ }
+ selectionHasChanged = true;
+
+ return true;
+ }
+
+ /**
+ * Select a range between two nodes which have no relation to each other
+ *
+ * @param startNode
+ * The start node to start the selection from
+ * @param endNode
+ * The end node to end the selection to
+ */
+ private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) {
+
+ TreeNode commonParent = getCommonGrandParent(startNode, endNode);
+ TreeNode startBranch = null, endBranch = null;
+
+ // Find the children of the common parent
+ List<TreeNode> children;
+ if (commonParent != null) {
+ children = commonParent.getChildren();
+ } else {
+ children = getRootNodes();
+ }
+
+ // Find the start and end branches
+ for (TreeNode node : children) {
+ if (nodeIsInBranch(startNode, node)) {
+ startBranch = node;
+ }
+ if (nodeIsInBranch(endNode, node)) {
+ endBranch = node;
+ }
+ }
+
+ // Swap nodes if necessary
+ if (children.indexOf(startBranch) > children.indexOf(endBranch)) {
+ TreeNode temp = startBranch;
+ startBranch = endBranch;
+ endBranch = temp;
+
+ temp = startNode;
+ startNode = endNode;
+ endNode = temp;
+ }
+
+ // Select all children under the start node
+ selectAllChildren(startNode, true);
+ TreeNode startParent = startNode.getParentNode();
+ TreeNode currentNode = startNode;
+ while (startParent != null && startParent != commonParent) {
+ List<TreeNode> startChildren = startParent.getChildren();
+ for (int i = startChildren.indexOf(currentNode) + 1; i < startChildren
+ .size(); i++) {
+ selectAllChildren(startChildren.get(i), true);
+ }
+
+ currentNode = startParent;
+ startParent = startParent.getParentNode();
+ }
+
+ // Select nodes until the end node is reached
+ for (int i = children.indexOf(startBranch) + 1; i <= children
+ .indexOf(endBranch); i++) {
+ selectAllChildrenUntil(children.get(i), endNode, true, true);
+ }
+
+ // Ensure end node was selected
+ endNode.setSelected(true);
+ selectedIds.add(endNode.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Examines the children of the branch node and returns true if a node is in
+ * that branch
+ *
+ * @param node
+ * The node to search for
+ * @param branch
+ * The branch to search in
+ * @return True if found, false if not found
+ */
+ private boolean nodeIsInBranch(TreeNode node, TreeNode branch) {
+ if (node == branch) {
+ return true;
+ }
+ for (TreeNode child : branch.getChildren()) {
+ if (child == node) {
+ return true;
+ }
+ if (!child.isLeaf() && child.getState()) {
+ if (nodeIsInBranch(node, child)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Selects a range of items which are in direct relation with each other.<br/>
+ * NOTE: The start node <b>MUST</b> be before the end node!
+ *
+ * @param startNode
+ *
+ * @param endNode
+ */
+ private void doRelationSelection(TreeNode startNode, TreeNode endNode) {
+ TreeNode currentNode = endNode;
+ while (currentNode != startNode) {
+ currentNode.setSelected(true);
+ selectedIds.add(currentNode.key);
+
+ // Traverse children above the selection
+ List<TreeNode> subChildren = currentNode.getParentNode()
+ .getChildren();
+ if (subChildren.size() > 1) {
+ selectNodeRange(subChildren.iterator().next().key,
+ currentNode.key);
+ } else if (subChildren.size() == 1) {
+ TreeNode n = subChildren.get(0);
+ n.setSelected(true);
+ selectedIds.add(n.key);
+ }
+
+ currentNode = currentNode.getParentNode();
+ }
+ startNode.setSelected(true);
+ selectedIds.add(startNode.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects a range of items which have the same parent.
+ *
+ * @param startNode
+ * The start node
+ * @param endNode
+ * The end node
+ */
+ private void doSiblingSelection(TreeNode startNode, TreeNode endNode) {
+ TreeNode parent = startNode.getParentNode();
+
+ List<TreeNode> children;
+ if (parent == null) {
+ // Topmost parent
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ // Swap start and end point if needed
+ if (children.indexOf(startNode) > children.indexOf(endNode)) {
+ TreeNode temp = startNode;
+ startNode = endNode;
+ endNode = temp;
+ }
+
+ Iterator<TreeNode> childIter = children.iterator();
+ boolean startFound = false;
+ while (childIter.hasNext()) {
+ TreeNode node = childIter.next();
+ if (node == startNode) {
+ startFound = true;
+ }
+
+ if (startFound && node != endNode && node.getState()) {
+ selectAllChildren(node, true);
+ } else if (startFound && node != endNode) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ }
+
+ if (node == endNode) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ break;
+ }
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Returns the first common parent of two nodes
+ *
+ * @param node1
+ * The first node
+ * @param node2
+ * The second node
+ * @return The common parent or null
+ */
+ public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) {
+ // If either one does not have a grand parent then return null
+ if (node1.getParentNode() == null || node2.getParentNode() == null) {
+ return null;
+ }
+
+ // If the nodes are parents of each other then return null
+ if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) {
+ return null;
+ }
+
+ // Get parents of node1
+ List<TreeNode> parents1 = new ArrayList<TreeNode>();
+ TreeNode parent1 = node1.getParentNode();
+ while (parent1 != null) {
+ parents1.add(parent1);
+ parent1 = parent1.getParentNode();
+ }
+
+ // Get parents of node2
+ List<TreeNode> parents2 = new ArrayList<TreeNode>();
+ TreeNode parent2 = node2.getParentNode();
+ while (parent2 != null) {
+ parents2.add(parent2);
+ parent2 = parent2.getParentNode();
+ }
+
+ // Search the parents for the first common parent
+ for (int i = 0; i < parents1.size(); i++) {
+ parent1 = parents1.get(i);
+ for (int j = 0; j < parents2.size(); j++) {
+ parent2 = parents2.get(j);
+ if (parent1 == parent2) {
+ return parent1;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the node currently in focus
+ *
+ * @param node
+ * The node to focus or null to remove the focus completely
+ * @param scrollIntoView
+ * Scroll the node into view
+ */
+ public void setFocusedNode(TreeNode node, boolean scrollIntoView) {
+ // Unfocus previously focused node
+ if (focusedNode != null) {
+ focusedNode.setFocused(false);
+ }
+
+ if (node != null) {
+ node.setFocused(true);
+ }
+
+ focusedNode = node;
+
+ if (node != null && scrollIntoView) {
+ /*
+ * Delay scrolling the focused node into view if we are still
+ * rendering. #5396
+ */
+ if (!rendering) {
+ node.scrollIntoView();
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ focusedNode.scrollIntoView();
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Focuses a node and scrolls it into view
+ *
+ * @param node
+ * The node to focus
+ */
+ public void setFocusedNode(TreeNode node) {
+ setFocusedNode(node, true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ @Override
+ public void onFocus(FocusEvent event) {
+ treeHasFocus = true;
+ // If no node has focus, focus the first item in the tree
+ if (focusedNode == null && lastSelection == null && selectable) {
+ setFocusedNode(getFirstRootNode(), false);
+ } else if (focusedNode != null && selectable) {
+ setFocusedNode(focusedNode, false);
+ } else if (lastSelection != null && selectable) {
+ setFocusedNode(lastSelection, false);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+ @Override
+ public void onBlur(BlurEvent event) {
+ treeHasFocus = false;
+ if (focusedNode != null) {
+ focusedNode.setFocused(false);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
+ * .gwt.event.dom.client.KeyPressEvent)
+ */
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ NativeEvent nativeEvent = event.getNativeEvent();
+ int keyCode = nativeEvent.getKeyCode();
+ if (keyCode == 0 && nativeEvent.getCharCode() == ' ') {
+ // Provide a keyCode for space to be compatible with FireFox
+ // keypress event
+ keyCode = CHARCODE_SPACE;
+ }
+ if (handleKeyNavigation(keyCode,
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (handleKeyNavigation(event.getNativeEvent().getKeyCode(),
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ /**
+ * Handles the keyboard navigation
+ *
+ * @param keycode
+ * The keycode of the pressed key
+ * @param ctrl
+ * Was ctrl pressed
+ * @param shift
+ * Was shift pressed
+ * @return Returns true if the key was handled, else false
+ */
+ protected boolean handleKeyNavigation(int keycode, boolean ctrl,
+ boolean shift) {
+ // Navigate down
+ if (keycode == getNavigationDownKey()) {
+ TreeNode node = null;
+ // If node is open and has children then move in to the children
+ if (!focusedNode.isLeaf() && focusedNode.getState()
+ && focusedNode.getChildren().size() > 0) {
+ node = focusedNode.getChildren().get(0);
+ }
+
+ // Else move down to the next sibling
+ else {
+ node = getNextSibling(focusedNode);
+ if (node == null) {
+ // Else jump to the parent and try to select the next
+ // sibling there
+ TreeNode current = focusedNode;
+ while (node == null && current.getParentNode() != null) {
+ node = getNextSibling(current.getParentNode());
+ current = current.getParentNode();
+ }
+ }
+ }
+
+ if (node != null) {
+ setFocusedNode(node);
+ if (selectable) {
+ if (!ctrl && !shift) {
+ selectNode(node, true);
+ } else if (shift && isMultiselect) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, node.key);
+ } else if (shift) {
+ selectNode(node, true);
+ }
+ }
+ }
+ return true;
+ }
+
+ // Navigate up
+ if (keycode == getNavigationUpKey()) {
+ TreeNode prev = getPreviousSibling(focusedNode);
+ TreeNode node = null;
+ if (prev != null) {
+ node = getLastVisibleChildInTree(prev);
+ } else if (focusedNode.getParentNode() != null) {
+ node = focusedNode.getParentNode();
+ }
+ if (node != null) {
+ setFocusedNode(node);
+ if (selectable) {
+ if (!ctrl && !shift) {
+ selectNode(node, true);
+ } else if (shift && isMultiselect) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, node.key);
+ } else if (shift) {
+ selectNode(node, true);
+ }
+ }
+ }
+ return true;
+ }
+
+ // Navigate left (close branch)
+ if (keycode == getNavigationLeftKey()) {
+ if (!focusedNode.isLeaf() && focusedNode.getState()) {
+ focusedNode.setState(false, true);
+ } else if (focusedNode.getParentNode() != null
+ && (focusedNode.isLeaf() || !focusedNode.getState())) {
+
+ if (ctrl || !selectable) {
+ setFocusedNode(focusedNode.getParentNode());
+ } else if (shift) {
+ doRelationSelection(focusedNode.getParentNode(),
+ focusedNode);
+ setFocusedNode(focusedNode.getParentNode());
+ } else {
+ focusAndSelectNode(focusedNode.getParentNode());
+ }
+ }
+ return true;
+ }
+
+ // Navigate right (open branch)
+ if (keycode == getNavigationRightKey()) {
+ if (!focusedNode.isLeaf() && !focusedNode.getState()) {
+ focusedNode.setState(true, true);
+ } else if (!focusedNode.isLeaf()) {
+ if (ctrl || !selectable) {
+ setFocusedNode(focusedNode.getChildren().get(0));
+ } else if (shift) {
+ setSelected(focusedNode, true);
+ setFocusedNode(focusedNode.getChildren().get(0));
+ setSelected(focusedNode, true);
+ } else {
+ focusAndSelectNode(focusedNode.getChildren().get(0));
+ }
+ }
+ return true;
+ }
+
+ // Selection
+ if (keycode == getNavigationSelectKey()) {
+ if (!focusedNode.isSelected()) {
+ selectNode(
+ focusedNode,
+ (!isMultiselect || multiSelectMode == MULTISELECT_MODE_SIMPLE)
+ && selectable);
+ } else {
+ deselectNode(focusedNode);
+ }
+ return true;
+ }
+
+ // Home selection
+ if (keycode == getNavigationStartKey()) {
+ TreeNode node = getFirstRootNode();
+ if (ctrl || !selectable) {
+ setFocusedNode(node);
+ } else if (shift) {
+ deselectAll();
+ selectNodeRange(focusedNode.key, node.key);
+ } else {
+ selectNode(node, true);
+ }
+ sendSelectionToServer();
+ return true;
+ }
+
+ // End selection
+ if (keycode == getNavigationEndKey()) {
+ TreeNode lastNode = getLastRootNode();
+ TreeNode node = getLastVisibleChildInTree(lastNode);
+ if (ctrl || !selectable) {
+ setFocusedNode(node);
+ } else if (shift) {
+ deselectAll();
+ selectNodeRange(focusedNode.key, node.key);
+ } else {
+ selectNode(node, true);
+ }
+ sendSelectionToServer();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void focusAndSelectNode(TreeNode node) {
+ /*
+ * Keyboard navigation doesn't work reliably if the tree is in
+ * multiselect mode as well as isNullSelectionAllowed = false. It first
+ * tries to deselect the old focused node, which fails since there must
+ * be at least one selection. After this the newly focused node is
+ * selected and we've ended up with two selected nodes even though we
+ * only navigated with the arrow keys.
+ *
+ * Because of this, we first select the next node and later de-select
+ * the old one.
+ */
+ TreeNode oldFocusedNode = focusedNode;
+ setFocusedNode(node);
+ setSelected(focusedNode, true);
+ setSelected(oldFocusedNode, false);
+ }
+
+ /**
+ * Traverses the tree to the bottom most child
+ *
+ * @param root
+ * The root of the tree
+ * @return The bottom most child
+ */
+ private TreeNode getLastVisibleChildInTree(TreeNode root) {
+ if (root.isLeaf() || !root.getState() || root.getChildren().size() == 0) {
+ return root;
+ }
+ List<TreeNode> children = root.getChildren();
+ return getLastVisibleChildInTree(children.get(children.size() - 1));
+ }
+
+ /**
+ * Gets the next sibling in the tree
+ *
+ * @param node
+ * The node to get the sibling for
+ * @return The sibling node or null if the node is the last sibling
+ */
+ private TreeNode getNextSibling(TreeNode node) {
+ TreeNode parent = node.getParentNode();
+ List<TreeNode> children;
+ if (parent == null) {
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ int idx = children.indexOf(node);
+ if (idx < children.size() - 1) {
+ return children.get(idx + 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the previous sibling in the tree
+ *
+ * @param node
+ * The node to get the sibling for
+ * @return The sibling node or null if the node is the first sibling
+ */
+ private TreeNode getPreviousSibling(TreeNode node) {
+ TreeNode parent = node.getParentNode();
+ List<TreeNode> children;
+ if (parent == null) {
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ int idx = children.indexOf(node);
+ if (idx > 0) {
+ return children.get(idx - 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Add this to the element mouse down event by using element.setPropertyJSO
+ * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
+ * when the mouse is depressed in the mouse up event.
+ *
+ * @return Returns the JSO preventing text selection
+ */
+ private native JavaScriptObject applyDisableTextSelectionIEHack()
+ /*-{
+ return function(){ return false; };
+ }-*/;
+
+ /**
+ * Get the key that moves the selection head upwards. By default it is the
+ * up arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection head downwards. By default it is the
+ * down arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that scrolls to the left in the table. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that scroll to the right on the table. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the space
+ * bar key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return CHARCODE_SPACE;
+ }
+
+ /**
+ * Get the key the moves the selection one page up in the table. By default
+ * this is the Page Up key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageUpKey() {
+ return KeyCodes.KEY_PAGEUP;
+ }
+
+ /**
+ * Get the key the moves the selection one page down in the table. By
+ * default this is the Page Down key but by overriding this you can change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageDownKey() {
+ return KeyCodes.KEY_PAGEDOWN;
+ }
+
+ /**
+ * Get the key the moves the selection to the beginning of the table. By
+ * default this is the Home key but by overriding this you can change the
+ * key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationStartKey() {
+ return KeyCodes.KEY_HOME;
+ }
+
+ /**
+ * Get the key the moves the selection to the end of the table. By default
+ * this is the End key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationEndKey() {
+ return KeyCodes.KEY_END;
+ }
+
+ private final String SUBPART_NODE_PREFIX = "n";
+ private final String EXPAND_IDENTIFIER = "expand";
+
+ /*
+ * In webkit, focus may have been requested for this component but not yet
+ * gained. Use this to trac if tree has gained the focus on webkit. See
+ * FocusImplSafari and #6373
+ */
+ private boolean treeHasFocus;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartElement(java
+ * .lang.String)
+ */
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if ("fe".equals(subPart)) {
+ if (BrowserInfo.get().isOpera() && focusedNode != null) {
+ return focusedNode.getElement();
+ }
+ return getFocusElement();
+ }
+
+ if (subPart.startsWith(SUBPART_NODE_PREFIX + "[")) {
+ boolean expandCollapse = false;
+
+ // Node
+ String[] nodes = subPart.split("/");
+ TreeNode treeNode = null;
+ try {
+ for (String node : nodes) {
+ if (node.startsWith(SUBPART_NODE_PREFIX)) {
+
+ // skip SUBPART_NODE_PREFIX"["
+ node = node.substring(SUBPART_NODE_PREFIX.length() + 1);
+ // skip "]"
+ node = node.substring(0, node.length() - 1);
+ int position = Integer.parseInt(node);
+ if (treeNode == null) {
+ treeNode = getRootNodes().get(position);
+ } else {
+ treeNode = treeNode.getChildren().get(position);
+ }
+ } else if (node.startsWith(EXPAND_IDENTIFIER)) {
+ expandCollapse = true;
+ }
+ }
+
+ if (expandCollapse) {
+ return treeNode.getElement();
+ } else {
+ return treeNode.nodeCaptionSpan;
+ }
+ } catch (Exception e) {
+ // Invalid locator string or node could not be found
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartName(com.google
+ * .gwt.user.client.Element)
+ */
+ @Override
+ public String getSubPartName(Element subElement) {
+ // Supported identifiers:
+ //
+ // n[index]/n[index]/n[index]{/expand}
+ //
+ // Ends with "/expand" if the target is expand/collapse indicator,
+ // otherwise ends with the node
+
+ boolean isExpandCollapse = false;
+
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ if (subElement == getFocusElement()) {
+ return "fe";
+ }
+
+ TreeNode treeNode = Util.findWidget(subElement, TreeNode.class);
+ if (treeNode == null) {
+ // Did not click on a node, let somebody else take care of the
+ // locator string
+ return null;
+ }
+
+ if (subElement == treeNode.getElement()) {
+ // Targets expand/collapse arrow
+ isExpandCollapse = true;
+ }
+
+ ArrayList<Integer> positions = new ArrayList<Integer>();
+ while (treeNode.getParentNode() != null) {
+ positions.add(0,
+ treeNode.getParentNode().getChildren().indexOf(treeNode));
+ treeNode = treeNode.getParentNode();
+ }
+ positions.add(0, getRootNodes().indexOf(treeNode));
+
+ String locator = "";
+ for (Integer i : positions) {
+ locator += SUBPART_NODE_PREFIX + "[" + i + "]/";
+ }
+
+ locator = locator.substring(0, locator.length() - 1);
+ if (isExpandCollapse) {
+ locator += "/" + EXPAND_IDENTIFIER;
+ }
+ return locator;
+ }
+
+ @Override
+ public Action[] getActions() {
+ if (bodyActionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[bodyActionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = bodyActionKeys[i];
+ final TreeAction a = new TreeAction(this, null, actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ private void handleBodyContextMenu(ContextMenuEvent event) {
+ if (!readonly && !disabled) {
+ if (bodyActionKeys != null) {
+ int left = event.getNativeEvent().getClientX();
+ int top = event.getNativeEvent().getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ public void registerAction(String key, String caption, String iconUrl) {
+ actionMap.put(key + "_c", caption);
+ if (iconUrl != null) {
+ actionMap.put(key + "_i", iconUrl);
+ } else {
+ actionMap.remove(key + "_i");
+ }
+
+ }
+
+ public void registerNode(TreeNode treeNode) {
+ keyToNode.put(treeNode.key, treeNode);
+ }
+
+ public void clearNodeToKeyMap() {
+ keyToNode.clear();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java
new file mode 100644
index 0000000000..06e916fbc9
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/TreeTableConnector.java
@@ -0,0 +1,95 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.treetable;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel;
+import com.vaadin.terminal.gwt.client.ui.table.TableConnector;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable.VScrollTableBody.VScrollTableRow;
+import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.PendingNavigationEvent;
+import com.vaadin.ui.TreeTable;
+
+@Connect(TreeTable.class)
+public class TreeTableConnector extends TableConnector {
+ public static final String ATTRIBUTE_HIERARCHY_COLUMN_INDEX = "hci";
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ FocusableScrollPanel widget = null;
+ int scrollPosition = 0;
+ if (getWidget().collapseRequest) {
+ widget = (FocusableScrollPanel) getWidget().getWidget(1);
+ scrollPosition = widget.getScrollPosition();
+ }
+ getWidget().animationsEnabled = uidl.getBooleanAttribute("animate");
+ getWidget().colIndexOfHierarchy = uidl
+ .hasAttribute(ATTRIBUTE_HIERARCHY_COLUMN_INDEX) ? uidl
+ .getIntAttribute(ATTRIBUTE_HIERARCHY_COLUMN_INDEX) : 0;
+ int oldTotalRows = getWidget().getTotalRows();
+ super.updateFromUIDL(uidl, client);
+ if (getWidget().collapseRequest) {
+ if (getWidget().collapsedRowKey != null
+ && getWidget().scrollBody != null) {
+ VScrollTableRow row = getWidget().getRenderedRowByKey(
+ getWidget().collapsedRowKey);
+ if (row != null) {
+ getWidget().setRowFocus(row);
+ getWidget().focus();
+ }
+ }
+
+ int scrollPosition2 = widget.getScrollPosition();
+ if (scrollPosition != scrollPosition2) {
+ widget.setScrollPosition(scrollPosition);
+ }
+
+ // check which rows are needed from the server and initiate a
+ // deferred fetch
+ getWidget().onScroll(null);
+ }
+ // Recalculate table size if collapse request, or if page length is zero
+ // (not sent by server) and row count changes (#7908).
+ if (getWidget().collapseRequest
+ || (!uidl.hasAttribute("pagelength") && getWidget()
+ .getTotalRows() != oldTotalRows)) {
+ /*
+ * Ensure that possibly removed/added scrollbars are considered.
+ * Triggers row calculations, removes cached rows etc. Basically
+ * cleans up state. Be careful if touching this, you will break
+ * pageLength=0 if you remove this.
+ */
+ getWidget().triggerLazyColumnAdjustment(true);
+
+ getWidget().collapseRequest = false;
+ }
+ if (uidl.hasAttribute("focusedRow")) {
+ String key = uidl.getStringAttribute("focusedRow");
+ getWidget().setRowFocus(getWidget().getRenderedRowByKey(key));
+ getWidget().focusParentResponsePending = false;
+ } else if (uidl.hasAttribute("clearFocusPending")) {
+ // Special case to detect a response to a focusParent request that
+ // does not return any focusedRow because the selected node has no
+ // parent
+ getWidget().focusParentResponsePending = false;
+ }
+
+ while (!getWidget().collapseRequest
+ && !getWidget().focusParentResponsePending
+ && !getWidget().pendingNavigationEvents.isEmpty()) {
+ // Keep replaying any queued events as long as we don't have any
+ // potential content changes pending
+ PendingNavigationEvent event = getWidget().pendingNavigationEvents
+ .removeFirst();
+ getWidget()
+ .handleNavigation(event.keycode, event.ctrl, event.shift);
+ }
+ }
+
+ @Override
+ public VTreeTable getWidget() {
+ return (VTreeTable) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java
new file mode 100644
index 0000000000..c03dff9507
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/treetable/VTreeTable.java
@@ -0,0 +1,830 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.treetable;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.ImageElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.event.dom.client.KeyCodes;
+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.Widget;
+import com.vaadin.terminal.gwt.client.ComputedStyle;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable;
+import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.VTreeTableScrollBody.VTreeTableRow;
+
+public class VTreeTable extends VScrollTable {
+
+ static class PendingNavigationEvent {
+ final int keycode;
+ final boolean ctrl;
+ final boolean shift;
+
+ public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) {
+ this.keycode = keycode;
+ this.ctrl = ctrl;
+ this.shift = shift;
+ }
+
+ @Override
+ public String toString() {
+ String string = "Keyboard event: " + keycode;
+ if (ctrl) {
+ string += " + ctrl";
+ }
+ if (shift) {
+ string += " + shift";
+ }
+ return string;
+ }
+ }
+
+ boolean collapseRequest;
+ private boolean selectionPending;
+ int colIndexOfHierarchy;
+ String collapsedRowKey;
+ VTreeTableScrollBody scrollBody;
+ boolean animationsEnabled;
+ LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>();
+ boolean focusParentResponsePending;
+
+ @Override
+ protected VScrollTableBody createScrollBody() {
+ scrollBody = new VTreeTableScrollBody();
+ return scrollBody;
+ }
+
+ /*
+ * Overridden to allow animation of expands and collapses of nodes.
+ */
+ @Override
+ protected void addAndRemoveRows(UIDL partialRowAdditions) {
+ if (partialRowAdditions == null) {
+ return;
+ }
+
+ if (animationsEnabled) {
+ if (partialRowAdditions.hasAttribute("hide")) {
+ scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ } else {
+ scrollBody.insertRowsAnimated(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ discardRowsOutsideCacheWindow();
+ }
+ } else {
+ super.addAndRemoveRows(partialRowAdditions);
+ }
+ }
+
+ class VTreeTableScrollBody extends VScrollTable.VScrollTableBody {
+ private int indentWidth = -1;
+
+ VTreeTableScrollBody() {
+ super();
+ }
+
+ @Override
+ protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
+ if (uidl.hasAttribute("gen_html")) {
+ // This is a generated row.
+ return new VTreeTableGeneratedRow(uidl, aligns2);
+ }
+ return new VTreeTableRow(uidl, aligns2);
+ }
+
+ class VTreeTableRow extends
+ VScrollTable.VScrollTableBody.VScrollTableRow {
+
+ private boolean isTreeCellAdded = false;
+ private SpanElement treeSpacer;
+ private boolean open;
+ private int depth;
+ private boolean canHaveChildren;
+ protected Widget widgetInHierarchyColumn;
+
+ public VTreeTableRow(UIDL uidl, char[] aligns2) {
+ super(uidl, aligns2);
+ }
+
+ @Override
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean isSorted,
+ String description) {
+ super.addCell(rowUidl, text, align, style, textIsHTML,
+ isSorted, description);
+
+ addTreeSpacer(rowUidl);
+ }
+
+ protected boolean addTreeSpacer(UIDL rowUidl) {
+ if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
+ Element container = (Element) getElement().getLastChild()
+ .getFirstChild();
+
+ if (rowUidl.hasAttribute("icon")) {
+ // icons are in first content cell in TreeTable
+ ImageElement icon = Document.get().createImageElement();
+ icon.setClassName("v-icon");
+ icon.setAlt("icon");
+ icon.setSrc(client.translateVaadinUri(rowUidl
+ .getStringAttribute("icon")));
+ container.insertFirst(icon);
+ }
+
+ String classname = "v-treetable-treespacer";
+ if (rowUidl.getBooleanAttribute("ca")) {
+ canHaveChildren = true;
+ open = rowUidl.getBooleanAttribute("open");
+ classname += open ? " v-treetable-node-open"
+ : " v-treetable-node-closed";
+ }
+
+ treeSpacer = Document.get().createSpanElement();
+
+ treeSpacer.setClassName(classname);
+ container.insertFirst(treeSpacer);
+ depth = rowUidl.hasAttribute("depth") ? rowUidl
+ .getIntAttribute("depth") : 0;
+ setIndent();
+ isTreeCellAdded = true;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean cellShowsTreeHierarchy(int curColIndex) {
+ if (isTreeCellAdded) {
+ return false;
+ }
+ return curColIndex == colIndexOfHierarchy
+ + (showRowHeaders ? 1 : 0);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (event.getEventTarget().cast() == treeSpacer
+ && treeSpacer.getClassName().contains("node")) {
+ if (event.getTypeInt() == Event.ONMOUSEUP) {
+ sendToggleCollapsedUpdate(getKey());
+ }
+ return;
+ }
+ super.onBrowserEvent(event);
+ }
+
+ @Override
+ public void addCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean isSorted) {
+ super.addCell(rowUidl, w, align, style, isSorted);
+ if (addTreeSpacer(rowUidl)) {
+ widgetInHierarchyColumn = w;
+ }
+
+ }
+
+ private void setIndent() {
+ if (getIndentWidth() > 0) {
+ treeSpacer.getParentElement().getStyle()
+ .setPaddingLeft(getIndent(), Unit.PX);
+ treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (getIndentWidth() < 0) {
+ detectIndent(this);
+ }
+ }
+
+ private int getHierarchyAndIconWidth() {
+ int consumedSpace = treeSpacer.getOffsetWidth();
+ if (treeSpacer.getParentElement().getChildCount() > 2) {
+ // icon next to tree spacer
+ consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
+ .getNextSibling()).getOffsetWidth();
+ }
+ return consumedSpace;
+ }
+
+ @Override
+ protected void setCellWidth(int cellIx, int width) {
+ if (cellIx == colIndexOfHierarchy + (showRowHeaders ? 1 : 0)) {
+ // take indentation padding into account if this is the
+ // hierarchy column
+ width = Math.max(width - getIndent(), 0);
+ }
+ super.setCellWidth(cellIx, width);
+ }
+
+ private int getIndent() {
+ return (depth + 1) * getIndentWidth();
+ }
+ }
+
+ protected class VTreeTableGeneratedRow extends VTreeTableRow {
+ private boolean spanColumns;
+ private boolean htmlContentAllowed;
+
+ public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
+ super(uidl, aligns);
+ addStyleName("v-table-generated-row");
+ }
+
+ public boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ @Override
+ protected void initCellWidths() {
+ if (spanColumns) {
+ setSpannedColumnWidthAfterDOMFullyInited();
+ } else {
+ super.initCellWidths();
+ }
+ }
+
+ private void setSpannedColumnWidthAfterDOMFullyInited() {
+ // Defer setting width on spanned columns to make sure that
+ // they are added to the DOM before trying to calculate
+ // widths.
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (showRowHeaders) {
+ setCellWidth(0, tHead.getHeaderCell(0).getWidth());
+ calcAndSetSpanWidthOnCell(1);
+ } else {
+ calcAndSetSpanWidthOnCell(0);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected boolean isRenderHtmlInCells() {
+ return htmlContentAllowed;
+ }
+
+ @Override
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
+ spanColumns = uidl.getBooleanAttribute("gen_span");
+
+ final Iterator<?> cells = uidl.getChildIterator();
+ if (spanColumns) {
+ int colCount = uidl.getChildCount();
+ if (cells.hasNext()) {
+ final Object cell = cells.next();
+ if (cell instanceof String) {
+ addSpannedCell(uidl, cell.toString(), aligns[0],
+ "", htmlContentAllowed, false, null,
+ colCount);
+ } else {
+ addSpannedCell(uidl, (Widget) cell, aligns[0], "",
+ false, colCount);
+ }
+ }
+ } else {
+ super.addCellsFromUIDL(uidl, aligns, col,
+ visibleColumnIndex);
+ }
+ }
+
+ private void addSpannedCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted, int colCount) {
+ TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithWidget(w, align, style, sorted, td);
+ td.getStyle().setHeight(getRowHeight(), Unit.PX);
+ if (addTreeSpacer(rowUidl)) {
+ widgetInHierarchyColumn = w;
+ }
+ }
+
+ private void addSpannedCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, int colCount) {
+ // String only content is optimized by not using Label widget
+ final TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithText(text, align, style, textIsHTML, sorted,
+ description, td);
+ td.getStyle().setHeight(getRowHeight(), Unit.PX);
+ addTreeSpacer(rowUidl);
+ }
+
+ @Override
+ protected void setCellWidth(int cellIx, int width) {
+ if (isSpanColumns()) {
+ if (showRowHeaders) {
+ if (cellIx == 0) {
+ super.setCellWidth(0, width);
+ } else {
+ // We need to recalculate the spanning TDs width for
+ // every cellIx in order to support column resizing.
+ calcAndSetSpanWidthOnCell(1);
+ }
+ } else {
+ // Same as above.
+ calcAndSetSpanWidthOnCell(0);
+ }
+ } else {
+ super.setCellWidth(cellIx, width);
+ }
+ }
+
+ private void calcAndSetSpanWidthOnCell(final int cellIx) {
+ int spanWidth = 0;
+ for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
+ .getVisibleCellCount(); ix++) {
+ spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
+ }
+ Util.setWidthExcludingPaddingAndBorder((Element) getElement()
+ .getChild(cellIx), spanWidth, 13, false);
+ }
+ }
+
+ private int getIndentWidth() {
+ return indentWidth;
+ }
+
+ private void detectIndent(VTreeTableRow vTreeTableRow) {
+ indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
+ if (indentWidth == 0) {
+ indentWidth = -1;
+ return;
+ }
+ Iterator<Widget> iterator = iterator();
+ while (iterator.hasNext()) {
+ VTreeTableRow next = (VTreeTableRow) iterator.next();
+ next.setIndent();
+ }
+ }
+
+ protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
+ final int firstIndex, final int rows) {
+ List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
+ for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
+ VScrollTableRow row = getRowByRowIndex(ix);
+ if (row != null) {
+ rowsToDelete.add(row);
+ }
+ }
+ if (!rowsToDelete.isEmpty()) {
+ // #8810 Only animate if there's something to animate
+ RowCollapseAnimation anim = new RowCollapseAnimation(
+ rowsToDelete) {
+ @Override
+ protected void onComplete() {
+ super.onComplete();
+ // Actually unlink the rows and update the cache after
+ // the
+ // animation is done.
+ unlinkAndReindexRows(firstIndex, rows);
+ discardRowsOutsideCacheWindow();
+ ensureCacheFilled();
+ }
+ };
+ anim.run(150);
+ }
+ }
+
+ protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
+ int firstIndex, int rows) {
+ List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
+ firstIndex, rows);
+ if (!insertedRows.isEmpty()) {
+ // Only animate if there's something to animate (#8810)
+ RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
+ anim.run(150);
+ }
+ return insertedRows;
+ }
+
+ /**
+ * Prepares the table for animation by copying the background colors of
+ * all TR elements to their respective TD elements if the TD element is
+ * transparent. This is needed, since if TDs have transparent
+ * backgrounds, the rows sliding behind them are visible.
+ */
+ private class AnimationPreparator {
+ private final int lastItemIx;
+
+ public AnimationPreparator(int lastItemIx) {
+ this.lastItemIx = lastItemIx;
+ }
+
+ public void prepareTableForAnimation() {
+ int ix = lastItemIx;
+ VScrollTableRow row = null;
+ while ((row = getRowByRowIndex(ix)) != null) {
+ copyTRBackgroundsToTDs(row);
+ --ix;
+ }
+ }
+
+ private void copyTRBackgroundsToTDs(VScrollTableRow row) {
+ Element tr = row.getElement();
+ ComputedStyle cs = new ComputedStyle(tr);
+ String backgroundAttachment = cs
+ .getProperty("backgroundAttachment");
+ String backgroundClip = cs.getProperty("backgroundClip");
+ String backgroundColor = cs.getProperty("backgroundColor");
+ String backgroundImage = cs.getProperty("backgroundImage");
+ String backgroundOrigin = cs.getProperty("backgroundOrigin");
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ Element td = tr.getChild(ix).cast();
+ if (!elementHasBackground(td)) {
+ td.getStyle().setProperty("backgroundAttachment",
+ backgroundAttachment);
+ td.getStyle().setProperty("backgroundClip",
+ backgroundClip);
+ td.getStyle().setProperty("backgroundColor",
+ backgroundColor);
+ td.getStyle().setProperty("backgroundImage",
+ backgroundImage);
+ td.getStyle().setProperty("backgroundOrigin",
+ backgroundOrigin);
+ }
+ }
+ }
+
+ private boolean elementHasBackground(Element element) {
+ ComputedStyle cs = new ComputedStyle(element);
+ String clr = cs.getProperty("backgroundColor");
+ String img = cs.getProperty("backgroundImage");
+ return !("rgba(0, 0, 0, 0)".equals(clr.trim())
+ || "transparent".equals(clr.trim()) || img == null);
+ }
+
+ public void restoreTableAfterAnimation() {
+ int ix = lastItemIx;
+ VScrollTableRow row = null;
+ while ((row = getRowByRowIndex(ix)) != null) {
+ restoreStyleForTDsInRow(row);
+
+ --ix;
+ }
+ }
+
+ private void restoreStyleForTDsInRow(VScrollTableRow row) {
+ Element tr = row.getElement();
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ Element td = tr.getChild(ix).cast();
+ td.getStyle().clearProperty("backgroundAttachment");
+ td.getStyle().clearProperty("backgroundClip");
+ td.getStyle().clearProperty("backgroundColor");
+ td.getStyle().clearProperty("backgroundImage");
+ td.getStyle().clearProperty("backgroundOrigin");
+ }
+ }
+ }
+
+ /**
+ * Animates row expansion using the GWT animation framework.
+ *
+ * The idea is as follows:
+ *
+ * 1. Insert all rows normally
+ *
+ * 2. Insert a newly created DIV containing a new TABLE element below
+ * the DIV containing the actual scroll table body.
+ *
+ * 3. Clone the rows that were inserted in step 1 and attach the clones
+ * to the new TABLE element created in step 2.
+ *
+ * 4. The new DIV from step 2 is absolutely positioned so that the last
+ * inserted row is just behind the row that was expanded.
+ *
+ * 5. Hide the contents of the originally inserted rows by setting the
+ * DIV.v-table-cell-wrapper to display:none;.
+ *
+ * 6. Set the height of the originally inserted rows to 0.
+ *
+ * 7. The animation loop slides the DIV from step 2 downwards, while at
+ * the same pace growing the height of each of the inserted rows from 0
+ * to full height. The first inserted row grows from 0 to full and after
+ * this the second row grows from 0 to full, etc until all rows are full
+ * height.
+ *
+ * 8. Remove the DIV from step 2
+ *
+ * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
+ *
+ * 10. DONE
+ */
+ private class RowExpandAnimation extends Animation {
+
+ private final List<VScrollTableRow> rows;
+ private Element cloneDiv;
+ private Element cloneTable;
+ private AnimationPreparator preparator;
+
+ /**
+ * @param rows
+ * List of rows to animate. Must not be empty.
+ */
+ public RowExpandAnimation(List<VScrollTableRow> rows) {
+ this.rows = rows;
+ buildAndInsertAnimatingDiv();
+ preparator = new AnimationPreparator(rows.get(0).getIndex() - 1);
+ preparator.prepareTableForAnimation();
+ for (VScrollTableRow row : rows) {
+ cloneAndAppendRow(row);
+ row.addStyleName("v-table-row-animating");
+ setCellWrapperDivsToDisplayNone(row);
+ row.setHeight(getInitialHeight());
+ }
+ }
+
+ protected String getInitialHeight() {
+ return "0px";
+ }
+
+ private void cloneAndAppendRow(VScrollTableRow row) {
+ Element clonedTR = null;
+ clonedTR = row.getElement().cloneNode(true).cast();
+ clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
+ cloneTable.appendChild(clonedTR);
+ }
+
+ protected double getBaseOffset() {
+ return rows.get(0).getAbsoluteTop()
+ - rows.get(0).getParent().getAbsoluteTop()
+ - rows.size() * getRowHeight();
+ }
+
+ private void buildAndInsertAnimatingDiv() {
+ cloneDiv = DOM.createDiv();
+ cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
+ cloneTable = DOM.createTable();
+ cloneTable.addClassName("v-treetable-animation-clone");
+ cloneDiv.appendChild(cloneTable);
+ insertAnimatingDiv();
+ }
+
+ private void insertAnimatingDiv() {
+ Element tableBody = getElement().cast();
+ Element tableBodyParent = tableBody.getParentElement().cast();
+ tableBodyParent.insertAfter(cloneDiv, tableBody);
+ }
+
+ @Override
+ protected void onUpdate(double progress) {
+ animateDiv(progress);
+ animateRowHeights(progress);
+ }
+
+ private void animateDiv(double progress) {
+ double offset = calculateDivOffset(progress, getRowHeight());
+
+ cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
+ }
+
+ private void animateRowHeights(double progress) {
+ double rh = getRowHeight();
+ double vlh = calculateHeightOfAllVisibleLines(progress, rh);
+ int ix = 0;
+
+ while (ix < rows.size()) {
+ double height = vlh < rh ? vlh : rh;
+ rows.get(ix).setHeight(height + "px");
+ vlh -= height;
+ ix++;
+ }
+ }
+
+ protected double calculateHeightOfAllVisibleLines(double progress,
+ double rh) {
+ return rows.size() * rh * progress;
+ }
+
+ protected double calculateDivOffset(double progress, double rh) {
+ return progress * rows.size() * rh;
+ }
+
+ @Override
+ protected void onComplete() {
+ preparator.restoreTableAfterAnimation();
+ for (VScrollTableRow row : rows) {
+ resetCellWrapperDivsDisplayProperty(row);
+ row.removeStyleName("v-table-row-animating");
+ }
+ Element tableBodyParent = (Element) getElement()
+ .getParentElement();
+ tableBodyParent.removeChild(cloneDiv);
+ }
+
+ private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) {
+ Element tr = row.getElement();
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ private Element getWrapperDiv(Element tr, int tdIx) {
+ Element td = tr.getChild(tdIx).cast();
+ return td.getChild(0).cast();
+ }
+
+ private void resetCellWrapperDivsDisplayProperty(VScrollTableRow row) {
+ Element tr = row.getElement();
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ getWrapperDiv(tr, ix).getStyle().clearProperty("display");
+ }
+ }
+
+ }
+
+ /**
+ * This is the inverse of the RowExpandAnimation and is implemented by
+ * extending it and overriding the calculation of offsets and heights.
+ */
+ private class RowCollapseAnimation extends RowExpandAnimation {
+
+ private final List<VScrollTableRow> rows;
+
+ /**
+ * @param rows
+ * List of rows to animate. Must not be empty.
+ */
+ public RowCollapseAnimation(List<VScrollTableRow> rows) {
+ super(rows);
+ this.rows = rows;
+ }
+
+ @Override
+ protected String getInitialHeight() {
+ return getRowHeight() + "px";
+ }
+
+ @Override
+ protected double getBaseOffset() {
+ return getRowHeight();
+ }
+
+ @Override
+ protected double calculateHeightOfAllVisibleLines(double progress,
+ double rh) {
+ return rows.size() * rh * (1 - progress);
+ }
+
+ @Override
+ protected double calculateDivOffset(double progress, double rh) {
+ return -super.calculateDivOffset(progress, rh);
+ }
+ }
+ }
+
+ /**
+ * Icons rendered into first actual column in TreeTable, not to row header
+ * cell
+ */
+ @Override
+ protected String buildCaptionHtmlSnippet(UIDL uidl) {
+ if (uidl.getTag().equals("column")) {
+ return super.buildCaptionHtmlSnippet(uidl);
+ } else {
+ String s = uidl.getStringAttribute("caption");
+ return s;
+ }
+ }
+
+ @Override
+ protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+ if (collapseRequest || focusParentResponsePending) {
+ // Enqueue the event if there might be pending content changes from
+ // the server
+ if (pendingNavigationEvents.size() < 10) {
+ // Only keep 10 keyboard events in the queue
+ PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
+ keycode, ctrl, shift);
+ pendingNavigationEvents.add(pendingNavigationEvent);
+ }
+ return true;
+ }
+
+ VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
+ if (focusedRow != null) {
+ if (focusedRow.canHaveChildren
+ && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
+ if (!ctrl) {
+ client.updateVariable(paintableId, "selectCollapsed", true,
+ false);
+ }
+ sendSelectedRows(false);
+ sendToggleCollapsedUpdate(focusedRow.getKey());
+ return true;
+ } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
+ // already expanded, move selection down if next is on a deeper
+ // level (is-a-child)
+ VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
+ .getParent();
+ Iterator<Widget> iterator = body.iterator();
+ VTreeTableRow next = null;
+ while (iterator.hasNext()) {
+ next = (VTreeTableRow) iterator.next();
+ if (next == focusedRow) {
+ next = (VTreeTableRow) iterator.next();
+ break;
+ }
+ }
+ if (next != null) {
+ if (next.depth > focusedRow.depth) {
+ selectionPending = true;
+ return super.handleNavigation(getNavigationDownKey(),
+ ctrl, shift);
+ }
+ } else {
+ // Note, a minor change here for a bit false behavior if
+ // cache rows is disabled + last visible row + no childs for
+ // the node
+ selectionPending = true;
+ return super.handleNavigation(getNavigationDownKey(), ctrl,
+ shift);
+ }
+ } else if (keycode == KeyCodes.KEY_LEFT) {
+ // already collapsed move selection up to parent node
+ // do on the server side as the parent is not necessary
+ // rendered on the client, could check if parent is visible if
+ // a performance issue arises
+
+ client.updateVariable(paintableId, "focusParent",
+ focusedRow.getKey(), true);
+
+ // Set flag that we should enqueue navigation events until we
+ // get a response to this request
+ focusParentResponsePending = true;
+
+ return true;
+ }
+ }
+ return super.handleNavigation(keycode, ctrl, shift);
+ }
+
+ private void sendToggleCollapsedUpdate(String rowKey) {
+ collapsedRowKey = rowKey;
+ collapseRequest = true;
+ client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
+ sendSelectedRows();
+ }
+ }
+
+ @Override
+ protected void sendSelectedRows(boolean immediately) {
+ super.sendSelectedRows(immediately);
+ selectionPending = false;
+ }
+
+ @Override
+ protected void reOrderColumn(String columnKey, int newIndex) {
+ super.reOrderColumn(columnKey, newIndex);
+ // current impl not intelligent enough to survive without visiting the
+ // server to redraw content
+ client.sendPendingVariableChanges();
+ }
+
+ @Override
+ public void setStyleName(String style) {
+ super.setStyleName(style + " v-treetable");
+ }
+
+ @Override
+ protected void updateTotalRows(UIDL uidl) {
+ // Make sure that initializedAndAttached & al are not reset when the
+ // totalrows are updated on expand/collapse requests.
+ int newTotalRows = uidl.getIntAttribute("totalrows");
+ setTotalRows(newTotalRows);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java
new file mode 100644
index 0000000000..2ce6bf2129
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/TwinColSelectConnector.java
@@ -0,0 +1,65 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.twincolselect;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.DirectionalManagedLayout;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.OptionGroupBaseConnector;
+import com.vaadin.ui.TwinColSelect;
+
+@Connect(TwinColSelect.class)
+public class TwinColSelectConnector extends OptionGroupBaseConnector implements
+ DirectionalManagedLayout {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Captions are updated before super call to ensure the widths are set
+ // correctly
+ if (isRealUpdate(uidl)) {
+ getWidget().updateCaptions(uidl);
+ getLayoutManager().setNeedsHorizontalLayout(this);
+ }
+
+ super.updateFromUIDL(uidl, client);
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getLayoutManager().registerDependency(this,
+ getWidget().captionWrapper.getElement());
+ }
+
+ @Override
+ public void onUnregister() {
+ getLayoutManager().unregisterDependency(this,
+ getWidget().captionWrapper.getElement());
+ }
+
+ @Override
+ public VTwinColSelect getWidget() {
+ return (VTwinColSelect) super.getWidget();
+ }
+
+ @Override
+ public void layoutVertically() {
+ if (isUndefinedHeight()) {
+ getWidget().clearInternalHeights();
+ } else {
+ getWidget().setInternalHeights();
+ }
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ if (isUndefinedWidth()) {
+ getWidget().clearInternalWidths();
+ } else {
+ getWidget().setInternalWidths();
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java
new file mode 100644
index 0000000000..1a2deae3b4
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/twincolselect/VTwinColSelect.java
@@ -0,0 +1,606 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.twincolselect;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.button.VButton;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroupBase;
+
+public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler,
+ MouseDownHandler, DoubleClickHandler, SubPartAware {
+
+ private static final String CLASSNAME = "v-select-twincol";
+ public static final String ATTRIBUTE_LEFT_CAPTION = "lc";
+ public static final String ATTRIBUTE_RIGHT_CAPTION = "rc";
+
+ private static final int VISIBLE_COUNT = 10;
+
+ private static final int DEFAULT_COLUMN_COUNT = 10;
+
+ private final DoubleClickListBox options;
+
+ private final DoubleClickListBox selections;
+
+ FlowPanel captionWrapper;
+
+ private HTML optionsCaption = null;
+
+ private HTML selectionsCaption = null;
+
+ private final VButton add;
+
+ private final VButton remove;
+
+ private final FlowPanel buttons;
+
+ private final Panel panel;
+
+ /**
+ * A ListBox which catches double clicks
+ *
+ */
+ public class DoubleClickListBox extends ListBox implements
+ HasDoubleClickHandlers {
+ public DoubleClickListBox(boolean isMultipleSelect) {
+ super(isMultipleSelect);
+ }
+
+ public DoubleClickListBox() {
+ super();
+ }
+
+ @Override
+ public HandlerRegistration addDoubleClickHandler(
+ DoubleClickHandler handler) {
+ return addDomHandler(handler, DoubleClickEvent.getType());
+ }
+ }
+
+ public VTwinColSelect() {
+ super(CLASSNAME);
+
+ captionWrapper = new FlowPanel();
+
+ options = new DoubleClickListBox();
+ options.addClickHandler(this);
+ options.addDoubleClickHandler(this);
+ options.setVisibleItemCount(VISIBLE_COUNT);
+ options.setStyleName(CLASSNAME + "-options");
+
+ selections = new DoubleClickListBox();
+ selections.addClickHandler(this);
+ selections.addDoubleClickHandler(this);
+ selections.setVisibleItemCount(VISIBLE_COUNT);
+ selections.setStyleName(CLASSNAME + "-selections");
+
+ buttons = new FlowPanel();
+ buttons.setStyleName(CLASSNAME + "-buttons");
+ add = new VButton();
+ add.setText(">>");
+ add.addClickHandler(this);
+ remove = new VButton();
+ remove.setText("<<");
+ remove.addClickHandler(this);
+
+ panel = ((Panel) optionsContainer);
+
+ panel.add(captionWrapper);
+ captionWrapper.getElement().getStyle().setOverflow(Overflow.HIDDEN);
+ // Hide until there actually is a caption to prevent IE from rendering
+ // extra empty space
+ captionWrapper.setVisible(false);
+
+ 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);
+
+ options.addKeyDownHandler(this);
+ options.addMouseDownHandler(this);
+
+ selections.addMouseDownHandler(this);
+ selections.addKeyDownHandler(this);
+ }
+
+ public HTML getOptionsCaption() {
+ if (optionsCaption == null) {
+ optionsCaption = new HTML();
+ optionsCaption.setStyleName(CLASSNAME + "-caption-left");
+ optionsCaption.getElement().getStyle()
+ .setFloat(com.google.gwt.dom.client.Style.Float.LEFT);
+ captionWrapper.add(optionsCaption);
+ }
+
+ return optionsCaption;
+ }
+
+ public HTML getSelectionsCaption() {
+ if (selectionsCaption == null) {
+ selectionsCaption = new HTML();
+ selectionsCaption.setStyleName(CLASSNAME + "-caption-right");
+ selectionsCaption.getElement().getStyle()
+ .setFloat(com.google.gwt.dom.client.Style.Float.RIGHT);
+ captionWrapper.add(selectionsCaption);
+ }
+
+ return selectionsCaption;
+ }
+
+ protected void updateCaptions(UIDL uidl) {
+ String leftCaption = (uidl.hasAttribute(ATTRIBUTE_LEFT_CAPTION) ? uidl
+ .getStringAttribute(ATTRIBUTE_LEFT_CAPTION) : null);
+ String rightCaption = (uidl.hasAttribute(ATTRIBUTE_RIGHT_CAPTION) ? uidl
+ .getStringAttribute(ATTRIBUTE_RIGHT_CAPTION) : null);
+
+ boolean hasCaptions = (leftCaption != null || rightCaption != null);
+
+ if (leftCaption == null) {
+ removeOptionsCaption();
+ } else {
+ getOptionsCaption().setText(leftCaption);
+
+ }
+
+ if (rightCaption == null) {
+ removeSelectionsCaption();
+ } else {
+ getSelectionsCaption().setText(rightCaption);
+ }
+
+ captionWrapper.setVisible(hasCaptions);
+ }
+
+ private void removeOptionsCaption() {
+ if (optionsCaption == null) {
+ return;
+ }
+
+ if (optionsCaption.getParent() != null) {
+ captionWrapper.remove(optionsCaption);
+ }
+
+ optionsCaption = null;
+ }
+
+ private void removeSelectionsCaption() {
+ if (selectionsCaption == null) {
+ return;
+ }
+
+ if (selectionsCaption.getParent() != null) {
+ captionWrapper.remove(selectionsCaption);
+ }
+
+ selectionsCaption = null;
+ }
+
+ @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"));
+ }
+ }
+
+ if (getRows() > 0) {
+ options.setVisibleItemCount(getRows());
+ selections.setVisibleItemCount(getRows());
+
+ }
+
+ }
+
+ @Override
+ protected String[] getSelectedItems() {
+ final ArrayList<String> selectedItemKeys = new ArrayList<String>();
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ selectedItemKeys.add(selections.getValue(i));
+ }
+ return selectedItemKeys.toArray(new String[selectedItemKeys.size()]);
+ }
+
+ private boolean[] getSelectionBitmap(ListBox listBox) {
+ final boolean[] selectedIndexes = new boolean[listBox.getItemCount()];
+ for (int i = 0; i < listBox.getItemCount(); i++) {
+ if (listBox.isItemSelected(i)) {
+ selectedIndexes[i] = true;
+ } else {
+ selectedIndexes[i] = false;
+ }
+ }
+ return selectedIndexes;
+ }
+
+ private void addItem() {
+ Set<String> movedItems = moveSelectedItems(options, selections);
+ selectedKeys.addAll(movedItems);
+
+ client.updateVariable(paintableId, "selected",
+ selectedKeys.toArray(new String[selectedKeys.size()]),
+ isImmediate());
+ }
+
+ private void removeItem() {
+ Set<String> movedItems = moveSelectedItems(selections, options);
+ selectedKeys.removeAll(movedItems);
+
+ client.updateVariable(paintableId, "selected",
+ selectedKeys.toArray(new String[selectedKeys.size()]),
+ isImmediate());
+ }
+
+ private Set<String> moveSelectedItems(ListBox source, ListBox target) {
+ final boolean[] sel = getSelectionBitmap(source);
+ final Set<String> movedItems = new HashSet<String>();
+ int lastSelected = 0;
+ for (int i = 0; i < sel.length; i++) {
+ if (sel[i]) {
+ final int optionIndex = i
+ - (sel.length - source.getItemCount());
+ movedItems.add(source.getValue(optionIndex));
+
+ // Move selection to another column
+ final String text = source.getItemText(optionIndex);
+ final String value = source.getValue(optionIndex);
+ target.addItem(text, value);
+ target.setItemSelected(target.getItemCount() - 1, true);
+ source.removeItem(optionIndex);
+
+ if (source.getItemCount() > 0) {
+ lastSelected = optionIndex > 0 ? optionIndex - 1 : 0;
+ }
+ }
+ }
+
+ if (source.getItemCount() > 0) {
+ source.setSelectedIndex(lastSelected);
+ }
+
+ // If no items are left move the focus to the selections
+ if (source.getItemCount() == 0) {
+ target.setFocus(true);
+ } else {
+ source.setFocus(true);
+ }
+
+ return movedItems;
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ super.onClick(event);
+ if (event.getSource() == add) {
+ addItem();
+
+ } else if (event.getSource() == remove) {
+ removeItem();
+
+ } else if (event.getSource() == 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 (event.getSource() == 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);
+ }
+ }
+ }
+
+ void clearInternalHeights() {
+ selections.setHeight("");
+ options.setHeight("");
+ }
+
+ void setInternalHeights() {
+ int captionHeight = Util.getRequiredHeight(captionWrapper);
+ int totalHeight = getOffsetHeight();
+
+ String selectHeight = (totalHeight - captionHeight) + "px";
+
+ selections.setHeight(selectHeight);
+ options.setHeight(selectHeight);
+
+ }
+
+ void clearInternalWidths() {
+ int cols = -1;
+ if (getColumns() > 0) {
+ cols = getColumns();
+ } else {
+ cols = DEFAULT_COLUMN_COUNT;
+ }
+
+ if (cols >= 0) {
+ String colWidth = cols + "em";
+ String containerWidth = (2 * cols + 4) + "em";
+ // Caption wrapper width == optionsSelect + buttons +
+ // selectionsSelect
+ String captionWrapperWidth = (2 * cols + 4 - 0.5) + "em";
+
+ options.setWidth(colWidth);
+ if (optionsCaption != null) {
+ optionsCaption.setWidth(colWidth);
+ }
+ selections.setWidth(colWidth);
+ if (selectionsCaption != null) {
+ selectionsCaption.setWidth(colWidth);
+ }
+ buttons.setWidth("3.5em");
+ optionsContainer.setWidth(containerWidth);
+ captionWrapper.setWidth(captionWrapperWidth);
+ }
+ }
+
+ void setInternalWidths() {
+ DOM.setStyleAttribute(getElement(), "position", "relative");
+ int bordersAndPaddings = Util.measureHorizontalPaddingAndBorder(
+ buttons.getElement(), 0);
+
+ int buttonWidth = Util.getRequiredWidth(buttons);
+ int totalWidth = getOffsetWidth();
+
+ int spaceForSelect = (totalWidth - buttonWidth - bordersAndPaddings) / 2;
+
+ options.setWidth(spaceForSelect + "px");
+ if (optionsCaption != null) {
+ optionsCaption.setWidth(spaceForSelect + "px");
+ }
+
+ selections.setWidth(spaceForSelect + "px");
+ if (selectionsCaption != null) {
+ selectionsCaption.setWidth(spaceForSelect + "px");
+ }
+ captionWrapper.setWidth("100%");
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ options.setTabIndex(tabIndex);
+ selections.setTabIndex(tabIndex);
+ add.setTabIndex(tabIndex);
+ remove.setTabIndex(tabIndex);
+ }
+
+ @Override
+ public void focus() {
+ options.setFocus(true);
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the Enter
+ * key but by overriding this you can change the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return KeyCodes.KEY_ENTER;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ int keycode = event.getNativeKeyCode();
+
+ // Catch tab and move between select:s
+ if (keycode == KeyCodes.KEY_TAB && event.getSource() == options) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Remove current selections
+ for (int i = 0; i < options.getItemCount(); i++) {
+ options.setItemSelected(i, false);
+ }
+
+ // Focus selections
+ selections.setFocus(true);
+ }
+
+ if (keycode == KeyCodes.KEY_TAB && event.isShiftKeyDown()
+ && event.getSource() == selections) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Remove current selections
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ selections.setItemSelected(i, false);
+ }
+
+ // Focus options
+ options.setFocus(true);
+ }
+
+ if (keycode == getNavigationSelectKey()) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Decide which select the selection was made in
+ if (event.getSource() == options) {
+ // Prevents the selection to become a single selection when
+ // using Enter key
+ // as the selection key (default)
+ options.setFocus(false);
+
+ addItem();
+
+ } else if (event.getSource() == selections) {
+ // Prevents the selection to become a single selection when
+ // using Enter key
+ // as the selection key (default)
+ selections.setFocus(false);
+
+ removeItem();
+ }
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google
+ * .gwt.event.dom.client.MouseDownEvent)
+ */
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ // Ensure that items are deselected when selecting
+ // from a different source. See #3699 for details.
+ if (event.getSource() == options) {
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ selections.setItemSelected(i, false);
+ }
+ } else if (event.getSource() == selections) {
+ for (int i = 0; i < options.getItemCount(); i++) {
+ options.setItemSelected(i, false);
+ }
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.DoubleClickHandler#onDoubleClick(com.
+ * google.gwt.event.dom.client.DoubleClickEvent)
+ */
+ @Override
+ public void onDoubleClick(DoubleClickEvent event) {
+ if (event.getSource() == options) {
+ addItem();
+ options.setSelectedIndex(-1);
+ options.setFocus(false);
+ } else if (event.getSource() == selections) {
+ removeItem();
+ selections.setSelectedIndex(-1);
+ selections.setFocus(false);
+ }
+
+ }
+
+ private static final String SUBPART_OPTION_SELECT = "leftSelect";
+ private static final String SUBPART_OPTION_SELECT_ITEM = SUBPART_OPTION_SELECT
+ + "-item";
+ private static final String SUBPART_SELECTION_SELECT = "rightSelect";
+ private static final String SUBPART_SELECTION_SELECT_ITEM = SUBPART_SELECTION_SELECT
+ + "-item";
+ private static final String SUBPART_LEFT_CAPTION = "leftCaption";
+ private static final String SUBPART_RIGHT_CAPTION = "rightCaption";
+ private static final String SUBPART_ADD_BUTTON = "add";
+ private static final String SUBPART_REMOVE_BUTTON = "remove";
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (SUBPART_OPTION_SELECT.equals(subPart)) {
+ return options.getElement();
+ } else if (subPart.startsWith(SUBPART_OPTION_SELECT_ITEM)) {
+ String idx = subPart.substring(SUBPART_OPTION_SELECT_ITEM.length());
+ return (Element) options.getElement().getChild(
+ Integer.parseInt(idx));
+ } else if (SUBPART_SELECTION_SELECT.equals(subPart)) {
+ return selections.getElement();
+ } else if (subPart.startsWith(SUBPART_SELECTION_SELECT_ITEM)) {
+ String idx = subPart.substring(SUBPART_SELECTION_SELECT_ITEM
+ .length());
+ return (Element) selections.getElement().getChild(
+ Integer.parseInt(idx));
+ } else if (optionsCaption != null
+ && SUBPART_LEFT_CAPTION.equals(subPart)) {
+ return optionsCaption.getElement();
+ } else if (selectionsCaption != null
+ && SUBPART_RIGHT_CAPTION.equals(subPart)) {
+ return selectionsCaption.getElement();
+ } else if (SUBPART_ADD_BUTTON.equals(subPart)) {
+ return add.getElement();
+ } else if (SUBPART_REMOVE_BUTTON.equals(subPart)) {
+ return remove.getElement();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (optionsCaption != null
+ && optionsCaption.getElement().isOrHasChild(subElement)) {
+ return SUBPART_LEFT_CAPTION;
+ } else if (selectionsCaption != null
+ && selectionsCaption.getElement().isOrHasChild(subElement)) {
+ return SUBPART_RIGHT_CAPTION;
+ } else if (options.getElement().isOrHasChild(subElement)) {
+ if (options.getElement() == subElement) {
+ return SUBPART_OPTION_SELECT;
+ } else {
+ int idx = Util.getChildElementIndex(subElement);
+ return SUBPART_OPTION_SELECT_ITEM + idx;
+ }
+ } else if (selections.getElement().isOrHasChild(subElement)) {
+ if (selections.getElement() == subElement) {
+ return SUBPART_SELECTION_SELECT;
+ } else {
+ int idx = Util.getChildElementIndex(subElement);
+ return SUBPART_SELECTION_SELECT_ITEM + idx;
+ }
+ } else if (add.getElement().isOrHasChild(subElement)) {
+ return SUBPART_ADD_BUTTON;
+ } else if (remove.getElement().isOrHasChild(subElement)) {
+ return SUBPART_REMOVE_BUTTON;
+ }
+
+ return null;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java
new file mode 100644
index 0000000000..af31491060
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadConnector.java
@@ -0,0 +1,61 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.upload;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+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.ui.AbstractComponentConnector;
+import com.vaadin.ui.Upload;
+
+@Connect(value = Upload.class, loadStyle = LoadStyle.LAZY)
+public class UploadConnector extends AbstractComponentConnector implements
+ Paintable {
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+ if (uidl.hasAttribute("notStarted")) {
+ getWidget().t.schedule(400);
+ return;
+ }
+ if (uidl.hasAttribute("forceSubmit")) {
+ getWidget().submit();
+ return;
+ }
+ getWidget().setImmediate(getState().isImmediate());
+ getWidget().client = client;
+ getWidget().paintableId = uidl.getId();
+ getWidget().nextUploadId = uidl.getIntAttribute("nextid");
+ final String action = client.translateVaadinUri(uidl
+ .getStringVariable("action"));
+ getWidget().element.setAction(action);
+ if (uidl.hasAttribute("buttoncaption")) {
+ getWidget().submitButton.setText(uidl
+ .getStringAttribute("buttoncaption"));
+ getWidget().submitButton.setVisible(true);
+ } else {
+ getWidget().submitButton.setVisible(false);
+ }
+ getWidget().fu.setName(getWidget().paintableId + "_file");
+
+ if (!isEnabled() || isReadOnly()) {
+ getWidget().disableUpload();
+ } else if (!uidl.getBooleanAttribute("state")) {
+ // Enable the button only if an upload is not in progress
+ getWidget().enableUpload();
+ getWidget().ensureTargetFrame();
+ }
+ }
+
+ @Override
+ public VUpload getWidget() {
+ return (VUpload) super.getWidget();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java
new file mode 100644
index 0000000000..174a4b88ca
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategy.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.upload;
+
+public class UploadIFrameOnloadStrategy {
+
+ native void hookEvents(com.google.gwt.dom.client.Element iframe,
+ VUpload upload)
+ /*-{
+ iframe.onload = $entry(function() {
+ upload.@com.vaadin.terminal.gwt.client.ui.upload.VUpload::onSubmitComplete()();
+ });
+ }-*/;
+
+ /**
+ * @param iframe
+ * the iframe whose onLoad event is to be cleaned
+ */
+ native void unHookEvents(com.google.gwt.dom.client.Element iframe)
+ /*-{
+ iframe.onload = null;
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java
new file mode 100644
index 0000000000..17a7e46bd5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/UploadIFrameOnloadStrategyIE.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.upload;
+
+import com.google.gwt.dom.client.Element;
+
+/**
+ * IE does not have onload, detect onload via readystatechange
+ *
+ */
+public class UploadIFrameOnloadStrategyIE extends UploadIFrameOnloadStrategy {
+ @Override
+ native void hookEvents(Element iframe, VUpload upload)
+ /*-{
+ iframe.onreadystatechange = $entry(function() {
+ if (iframe.readyState == 'complete') {
+ upload.@com.vaadin.terminal.gwt.client.ui.upload.VUpload::onSubmitComplete()();
+ }
+ });
+ }-*/;
+
+ @Override
+ native void unHookEvents(Element iframe)
+ /*-{
+ iframe.onreadystatechange = null;
+ }-*/;
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java b/client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java
new file mode 100644
index 0000000000..5cf2f2fe25
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/upload/VUpload.java
@@ -0,0 +1,307 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.upload;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.FormElement;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.Command;
+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.FileUpload;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.Hidden;
+import com.google.gwt.user.client.ui.Panel;
+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.VConsole;
+import com.vaadin.terminal.gwt.client.ui.button.VButton;
+
+/**
+ *
+ * Note, we are not using GWT FormPanel as we want to listen submitcomplete
+ * events even though the upload component is already detached.
+ *
+ */
+public class VUpload extends SimplePanel {
+
+ private final class MyFileUpload extends FileUpload {
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONCHANGE) {
+ if (immediate && fu.getFilename() != null
+ && !"".equals(fu.getFilename())) {
+ submit();
+ }
+ } else if (BrowserInfo.get().isIE()
+ && event.getTypeInt() == Event.ONFOCUS) {
+ // IE and user has clicked on hidden textarea part of upload
+ // field. Manually open file selector, other browsers do it by
+ // default.
+ fireNativeClick(fu.getElement());
+ // also remove focus to enable hack if user presses cancel
+ // button
+ fireNativeBlur(fu.getElement());
+ }
+ }
+ }
+
+ public static final String CLASSNAME = "v-upload";
+
+ /**
+ * FileUpload component that opens native OS dialog to select file.
+ */
+ FileUpload fu = new MyFileUpload();
+
+ Panel panel = new FlowPanel();
+
+ UploadIFrameOnloadStrategy onloadstrategy = GWT
+ .create(UploadIFrameOnloadStrategy.class);
+
+ ApplicationConnection client;
+
+ protected String paintableId;
+
+ /**
+ * Button that initiates uploading
+ */
+ protected final VButton 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.
+ */
+ protected 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;
+
+ private boolean immediate;
+
+ private Hidden maxfilesize = new Hidden();
+
+ protected FormElement element;
+
+ private com.google.gwt.dom.client.Element synthesizedFrame;
+
+ protected int nextUploadId;
+
+ public VUpload() {
+ super(com.google.gwt.dom.client.Document.get().createFormElement());
+
+ element = getElement().cast();
+ setEncoding(getElement(), FormPanel.ENCODING_MULTIPART);
+ element.setMethod(FormPanel.METHOD_POST);
+
+ setWidget(panel);
+ panel.add(maxfilesize);
+ panel.add(fu);
+ submitButton = new VButton();
+ submitButton.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (immediate) {
+ // fire click on upload (eg. focused button and hit space)
+ fireNativeClick(fu.getElement());
+ } else {
+ submit();
+ }
+ }
+ });
+ panel.add(submitButton);
+
+ setStyleName(CLASSNAME);
+ }
+
+ private static native void setEncoding(Element form, String encoding)
+ /*-{
+ form.enctype = encoding;
+ }-*/;
+
+ protected void setImmediate(boolean booleanAttribute) {
+ if (immediate != booleanAttribute) {
+ immediate = booleanAttribute;
+ if (immediate) {
+ fu.sinkEvents(Event.ONCHANGE);
+ fu.sinkEvents(Event.ONFOCUS);
+ }
+ }
+ setStyleName(getElement(), CLASSNAME + "-immediate", immediate);
+ }
+
+ private static native void fireNativeClick(Element element)
+ /*-{
+ element.click();
+ }-*/;
+
+ private static native void fireNativeBlur(Element element)
+ /*-{
+ element.blur();
+ }-*/;
+
+ protected void disableUpload() {
+ submitButton.setEnabled(false);
+ if (!submitted) {
+ // Cannot disable the fileupload while submitting or the file won't
+ // be submitted at all
+ fu.getElement().setPropertyBoolean("disabled", true);
+ }
+ enabled = false;
+ }
+
+ protected void enableUpload() {
+ submitButton.setEnabled(true);
+ fu.getElement().setPropertyBoolean("disabled", false);
+ enabled = true;
+ if (submitted) {
+ /*
+ * An old request is still in progress (most likely cancelled),
+ * ditching that target frame to make it possible to send a new
+ * file. A new target frame is created later."
+ */
+ cleanTargetFrame();
+ submitted = false;
+ }
+ }
+
+ /**
+ * Re-creates file input field and populates panel. This is needed as we
+ * want to clear existing values from our current file input field.
+ */
+ private void rebuildPanel() {
+ panel.remove(submitButton);
+ panel.remove(fu);
+ fu = new MyFileUpload();
+ fu.setName(paintableId + "_file");
+ fu.getElement().setPropertyBoolean("disabled", !enabled);
+ panel.add(fu);
+ panel.add(submitButton);
+ if (immediate) {
+ fu.sinkEvents(Event.ONCHANGE);
+ }
+ }
+
+ /**
+ * Called by JSNI (hooked via {@link #onloadstrategy})
+ */
+ private void onSubmitComplete() {
+ /* Needs to be run dereferred to avoid various browser issues. */
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ if (submitted) {
+ if (client != null) {
+ if (t != null) {
+ t.cancel();
+ }
+ VConsole.log("VUpload:Submit complete");
+ client.sendPendingVariableChanges();
+ }
+
+ rebuildPanel();
+
+ submitted = false;
+ enableUpload();
+ if (!isAttached()) {
+ /*
+ * Upload is complete when upload is already abandoned.
+ */
+ cleanTargetFrame();
+ }
+ }
+ }
+ });
+ }
+
+ protected void submit() {
+ if (fu.getFilename().length() == 0 || submitted || !enabled) {
+ VConsole.log("Submit cancelled (disabled, no file or already submitted)");
+ return;
+ }
+ // flush possibly pending variable changes, so they will be handled
+ // before upload
+ client.sendPendingVariableChanges();
+
+ element.submit();
+ submitted = true;
+ VConsole.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() {
+ VConsole.log("Visiting server to see if upload started event changed UI.");
+ client.updateVariable(paintableId, "pollForStart",
+ nextUploadId, true);
+ }
+ };
+ t.schedule(800);
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (client != null) {
+ ensureTargetFrame();
+ }
+ }
+
+ protected void ensureTargetFrame() {
+ if (synthesizedFrame == null) {
+ // Attach a hidden IFrame to the form. This is the target iframe to
+ // which the form will be submitted. We have to create the iframe
+ // using innerHTML, because setting an iframe's 'name' property
+ // dynamically doesn't work on most browsers.
+ DivElement dummy = Document.get().createDivElement();
+ dummy.setInnerHTML("<iframe src=\"javascript:''\" name='"
+ + getFrameName()
+ + "' style='position:absolute;width:0;height:0;border:0'>");
+ synthesizedFrame = dummy.getFirstChildElement();
+ Document.get().getBody().appendChild(synthesizedFrame);
+ element.setTarget(getFrameName());
+ onloadstrategy.hookEvents(synthesizedFrame, this);
+ }
+ }
+
+ private String getFrameName() {
+ return paintableId + "_TGT_FRAME";
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ if (!submitted) {
+ cleanTargetFrame();
+ }
+ }
+
+ private void cleanTargetFrame() {
+ if (synthesizedFrame != null) {
+ Document.get().getBody().removeChild(synthesizedFrame);
+ onloadstrategy.unHookEvents(synthesizedFrame);
+ synthesizedFrame = null;
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java b/client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java
new file mode 100644
index 0000000000..a2a4cd0ce3
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/video/VVideo.java
@@ -0,0 +1,59 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.video;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.VideoElement;
+import com.google.gwt.user.client.Element;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.VMediaBase;
+
+public class VVideo extends VMediaBase {
+
+ private static String CLASSNAME = "v-video";
+
+ private VideoElement video;
+
+ public VVideo() {
+ video = Document.get().createVideoElement();
+ setMediaElement(video);
+ setStyleName(CLASSNAME);
+
+ updateDimensionsWhenMetadataLoaded(getElement());
+ }
+
+ /**
+ * Registers a listener that updates the dimensions of the widget when the
+ * video metadata has been loaded.
+ *
+ * @param el
+ */
+ private native void updateDimensionsWhenMetadataLoaded(Element el)
+ /*-{
+ var self = this;
+ el.addEventListener('loadedmetadata', $entry(function(e) {
+ self.@com.vaadin.terminal.gwt.client.ui.video.VVideo::updateElementDynamicSize(II)(el.videoWidth, el.videoHeight);
+ }), false);
+
+ }-*/;
+
+ /**
+ * Updates the dimensions of the widget.
+ *
+ * @param w
+ * @param h
+ */
+ private void updateElementDynamicSize(int w, int h) {
+ video.getStyle().setWidth(w, Unit.PX);
+ video.getStyle().setHeight(h, Unit.PX);
+ Util.notifyParentOfSizeChange(this, true);
+ }
+
+ public void setPoster(String poster) {
+ video.setPoster(poster);
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java
new file mode 100644
index 0000000000..d0c126832a
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/video/VideoConnector.java
@@ -0,0 +1,42 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.video;
+
+import com.vaadin.shared.communication.URLReference;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.video.VideoState;
+import com.vaadin.terminal.gwt.client.communication.StateChangeEvent;
+import com.vaadin.terminal.gwt.client.ui.MediaBaseConnector;
+import com.vaadin.ui.Video;
+
+@Connect(Video.class)
+public class VideoConnector extends MediaBaseConnector {
+
+ @Override
+ public VideoState getState() {
+ return (VideoState) super.getState();
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ URLReference poster = getState().getPoster();
+ if (poster != null) {
+ getWidget().setPoster(poster.getURL());
+ } else {
+ getWidget().setPoster(null);
+ }
+ }
+
+ @Override
+ public VVideo getWidget() {
+ return (VVideo) super.getWidget();
+ }
+
+ @Override
+ protected String getDefaultAltHtml() {
+ return "Your browser does not support the <code>video</code> element.";
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java b/client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java
new file mode 100644
index 0000000000..8ac0d0662b
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/window/VWindow.java
@@ -0,0 +1,920 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.window;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+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.HasWidgets;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.EventId;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.Console;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.FocusableScrollPanel;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.VOverlay;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+
+/**
+ * "Sub window" component.
+ *
+ * @author Vaadin Ltd
+ */
+public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
+ ScrollHandler, KeyDownHandler, FocusHandler, BlurHandler, Focusable {
+
+ /**
+ * Minimum allowed height of a window. This refers to the content area, not
+ * the outer borders.
+ */
+ private static final int MIN_CONTENT_AREA_HEIGHT = 100;
+
+ /**
+ * Minimum allowed width of a window. This refers to the content area, not
+ * the outer borders.
+ */
+ private static final int MIN_CONTENT_AREA_WIDTH = 150;
+
+ private static ArrayList<VWindow> windowOrder = new ArrayList<VWindow>();
+
+ private static boolean orderingDefered;
+
+ public static final String CLASSNAME = "v-window";
+
+ private static final int STACKING_OFFSET_PIXELS = 15;
+
+ public static final int Z_INDEX = 10000;
+
+ ComponentConnector layout;
+
+ Element contents;
+
+ Element header;
+
+ Element footer;
+
+ private Element resizeBox;
+
+ final FocusableScrollPanel contentPanel = new FocusableScrollPanel();
+
+ private boolean dragging;
+
+ private int startX;
+
+ private int startY;
+
+ private int origX;
+
+ private int origY;
+
+ private boolean resizing;
+
+ private int origW;
+
+ private int origH;
+
+ Element closeBox;
+
+ protected ApplicationConnection client;
+
+ 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;
+
+ boolean vaadinModality = false;
+
+ boolean resizable = true;
+
+ private boolean draggable = true;
+
+ boolean resizeLazy = false;
+
+ private Element modalityCurtain;
+ private Element draggingCurtain;
+ private Element resizingCurtain;
+
+ private Element headerText;
+
+ private boolean closable = true;
+
+ // 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;
+
+ boolean immediate;
+
+ private Element wrapper;
+
+ boolean visibilityChangesDisabled;
+
+ int bringToFrontSequence = -1;
+
+ private VLazyExecutor delayedContentsSizeUpdater = new VLazyExecutor(200,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ updateContentsSize();
+ }
+ });
+
+ public VWindow() {
+ super(false, false, true); // no autohide, not modal, shadow
+ // Different style of shadow for windows
+ setShadowStyle("window");
+
+ constructDOM();
+ contentPanel.addScrollHandler(this);
+ contentPanel.addKeyDownHandler(this);
+ contentPanel.addFocusHandler(this);
+ contentPanel.addBlurHandler(this);
+ }
+
+ public 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 this window is the topmost VWindow
+ *
+ * @return
+ */
+ private boolean isActive() {
+ return equals(getTopmostWindow());
+ }
+
+ private static VWindow getTopmostWindow() {
+ return windowOrder.get(windowOrder.size() - 1);
+ }
+
+ void setWindowOrderAndPosition() {
+ // This cannot be done in the constructor as the widgets are created in
+ // a different order than on they should appear on screen
+ if (windowOrder.contains(this)) {
+ // Already set
+ return;
+ }
+ final int order = windowOrder.size();
+ setWindowOrder(order);
+ windowOrder.add(this);
+ setPopupPosition(order * STACKING_OFFSET_PIXELS, order
+ * STACKING_OFFSET_PIXELS);
+
+ }
+
+ private void setWindowOrder(int order) {
+ setZIndex(order + Z_INDEX);
+ }
+
+ @Override
+ protected void setZIndex(int zIndex) {
+ super.setZIndex(zIndex);
+ if (vaadinModality) {
+ DOM.setStyleAttribute(getModalityCurtain(), "zIndex", "" + zIndex);
+ }
+ }
+
+ protected Element getModalityCurtain() {
+ if (modalityCurtain == null) {
+ modalityCurtain = DOM.createDiv();
+ modalityCurtain.setClassName(CLASSNAME + "-modalitycurtain");
+ }
+ return modalityCurtain;
+ }
+
+ 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);
+
+ wrapper = DOM.createDiv();
+ DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap");
+
+ DOM.appendChild(wrapper, header);
+ DOM.appendChild(wrapper, closeBox);
+ DOM.appendChild(header, headerText);
+ DOM.appendChild(wrapper, contents);
+ DOM.appendChild(wrapper, footer);
+ DOM.appendChild(super.getContainerElement(), wrapper);
+
+ sinkEvents(Event.MOUSEEVENTS | Event.TOUCHEVENTS | Event.ONCLICK
+ | Event.ONLOSECAPTURE);
+
+ setWidget(contentPanel);
+
+ }
+
+ /**
+ * Calling this method will defer ordering algorithm, to order windows based
+ * on servers bringToFront and modality instructions. Non changed windows
+ * will be left intact.
+ */
+ static void deferOrdering() {
+ if (!orderingDefered) {
+ orderingDefered = true;
+ Scheduler.get().scheduleFinally(new Command() {
+
+ @Override
+ public void execute() {
+ doServerSideOrdering();
+ VNotification.bringNotificationsToFront();
+ }
+ });
+ }
+ }
+
+ private static void doServerSideOrdering() {
+ orderingDefered = false;
+ VWindow[] array = windowOrder.toArray(new VWindow[windowOrder.size()]);
+ Arrays.sort(array, new Comparator<VWindow>() {
+
+ @Override
+ public int compare(VWindow o1, VWindow o2) {
+ /*
+ * Order by modality, then by bringtofront sequence.
+ */
+
+ if (o1.vaadinModality && !o2.vaadinModality) {
+ return 1;
+ } else if (!o1.vaadinModality && o2.vaadinModality) {
+ return -1;
+ } else if (o1.bringToFrontSequence > o2.bringToFrontSequence) {
+ return 1;
+ } else if (o1.bringToFrontSequence < o2.bringToFrontSequence) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ for (int i = 0; i < array.length; i++) {
+ VWindow w = array[i];
+ if (w.bringToFrontSequence != -1 || w.vaadinModality) {
+ w.bringToFront();
+ w.bringToFrontSequence = -1;
+ }
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ /*
+ * Visibility with VWindow works differently than with other Paintables
+ * in Vaadin. Invisible VWindows are not attached to DOM at all. Flag is
+ * used to avoid visibility call from
+ * ApplicationConnection.updateComponent();
+ */
+ if (!visibilityChangesDisabled) {
+ super.setVisible(visible);
+ }
+ }
+
+ void setDraggable(boolean draggable) {
+ if (this.draggable == draggable) {
+ return;
+ }
+
+ this.draggable = draggable;
+
+ setCursorProperties();
+ }
+
+ private void setCursorProperties() {
+ if (!draggable) {
+ header.getStyle().setProperty("cursor", "default");
+ footer.getStyle().setProperty("cursor", "default");
+ } else {
+ header.getStyle().setProperty("cursor", "");
+ footer.getStyle().setProperty("cursor", "");
+ }
+ }
+
+ /**
+ * Sets the closable state of the window. Additionally hides/shows the close
+ * button according to the new state.
+ *
+ * @param closable
+ * true if the window can be closed by the user
+ */
+ protected void setClosable(boolean closable) {
+ if (this.closable == closable) {
+ return;
+ }
+
+ this.closable = closable;
+ if (closable) {
+ DOM.setStyleAttribute(closeBox, "display", "");
+ } else {
+ DOM.setStyleAttribute(closeBox, "display", "none");
+ }
+
+ }
+
+ /**
+ * Returns the closable state of the sub window. If the sub window is
+ * closable a decoration (typically an X) is shown to the user. By clicking
+ * on the X the user can close the window.
+ *
+ * @return true if the sub window is closable
+ */
+ protected boolean isClosable() {
+ return closable;
+ }
+
+ @Override
+ public void show() {
+ if (!windowOrder.contains(this)) {
+ // This is needed if the window is hidden and then shown again.
+ // Otherwise this VWindow is added to windowOrder in the
+ // constructor.
+ windowOrder.add(this);
+ }
+
+ if (vaadinModality) {
+ showModalityCurtain();
+ }
+ super.show();
+ }
+
+ @Override
+ public void hide() {
+ if (vaadinModality) {
+ hideModalityCurtain();
+ }
+ super.hide();
+
+ // Remove window from windowOrder to avoid references being left
+ // hanging.
+ windowOrder.remove(this);
+ }
+
+ void setVaadinModality(boolean modality) {
+ vaadinModality = modality;
+ if (vaadinModality) {
+ if (isAttached()) {
+ showModalityCurtain();
+ }
+ deferOrdering();
+ } else {
+ if (modalityCurtain != null) {
+ if (isAttached()) {
+ hideModalityCurtain();
+ }
+ modalityCurtain = null;
+ }
+ }
+ }
+
+ private void showModalityCurtain() {
+ DOM.setStyleAttribute(getModalityCurtain(), "zIndex",
+ "" + (windowOrder.indexOf(this) + Z_INDEX));
+ if (isShowing()) {
+ RootPanel.getBodyElement().insertBefore(getModalityCurtain(),
+ getElement());
+ } else {
+ DOM.appendChild(RootPanel.getBodyElement(), getModalityCurtain());
+ }
+ }
+
+ private void hideModalityCurtain() {
+ DOM.removeChild(RootPanel.getBodyElement(), modalityCurtain);
+ }
+
+ /*
+ * Shows an empty div on top of all other content; used when moving, so that
+ * iframes (etc) do not steal event.
+ */
+ private void showDraggingCurtain() {
+ DOM.appendChild(RootPanel.getBodyElement(), getDraggingCurtain());
+ }
+
+ private void hideDraggingCurtain() {
+ if (draggingCurtain != null) {
+ DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain);
+ }
+ }
+
+ /*
+ * Shows an empty div on top of all other content; used when resizing, so
+ * that iframes (etc) do not steal event.
+ */
+ private void showResizingCurtain() {
+ DOM.appendChild(RootPanel.getBodyElement(), getResizingCurtain());
+ }
+
+ private void hideResizingCurtain() {
+ if (resizingCurtain != null) {
+ DOM.removeChild(RootPanel.getBodyElement(), resizingCurtain);
+ }
+ }
+
+ private Element getDraggingCurtain() {
+ if (draggingCurtain == null) {
+ draggingCurtain = createCurtain();
+ draggingCurtain.setClassName(CLASSNAME + "-draggingCurtain");
+ }
+
+ return draggingCurtain;
+ }
+
+ private Element getResizingCurtain() {
+ if (resizingCurtain == null) {
+ resizingCurtain = createCurtain();
+ resizingCurtain.setClassName(CLASSNAME + "-resizingCurtain");
+ }
+
+ return resizingCurtain;
+ }
+
+ private Element createCurtain() {
+ Element curtain = DOM.createDiv();
+
+ DOM.setStyleAttribute(curtain, "position", "absolute");
+ DOM.setStyleAttribute(curtain, "top", "0px");
+ DOM.setStyleAttribute(curtain, "left", "0px");
+ DOM.setStyleAttribute(curtain, "width", "100%");
+ DOM.setStyleAttribute(curtain, "height", "100%");
+ DOM.setStyleAttribute(curtain, "zIndex", "" + VOverlay.Z_INDEX);
+
+ return curtain;
+ }
+
+ void setResizable(boolean resizability) {
+ resizable = resizability;
+ if (resizability) {
+ DOM.setElementProperty(footer, "className", CLASSNAME + "-footer");
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME
+ + "-resizebox");
+ } else {
+ DOM.setElementProperty(footer, "className", CLASSNAME + "-footer "
+ + CLASSNAME + "-footer-noresize");
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME
+ + "-resizebox " + CLASSNAME + "-resizebox-disabled");
+ }
+ }
+
+ @Override
+ public void setPopupPosition(int left, int top) {
+ if (top < 0) {
+ // ensure window is not moved out of browser window from top of the
+ // screen
+ top = 0;
+ }
+ 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.translateVaadinUri(icon);
+ html = "<img src=\"" + Util.escapeAttribute(icon)
+ + "\" class=\"v-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) {
+ boolean bubble = true;
+
+ final int type = event.getTypeInt();
+
+ final Element target = DOM.eventGetTarget(event);
+
+ if (resizing || resizeBox == target) {
+ onResizeEvent(event);
+ bubble = false;
+ } else if (isClosable() && target == closeBox) {
+ if (type == Event.ONCLICK) {
+ onCloseClick();
+ }
+ bubble = false;
+ } else if (dragging || !contents.isOrHasChild(target)) {
+ onDragEvent(event);
+ bubble = false;
+ } else if (type == Event.ONCLICK) {
+ // clicked inside window, ensure to be on top
+ if (!isActive()) {
+ bringToFront();
+ }
+ }
+
+ /*
+ * If clicking on other than the content, move focus to the window.
+ * After that this windows e.g. gets all keyboard shortcuts.
+ */
+ if (type == Event.ONMOUSEDOWN
+ && !contentPanel.getElement().isOrHasChild(target)
+ && target != closeBox) {
+ contentPanel.focus();
+ }
+
+ if (!bubble) {
+ event.stopPropagation();
+ } else {
+ // Super.onBrowserEvent takes care of Handlers added by the
+ // ClickEventHandler
+ super.onBrowserEvent(event);
+ }
+ }
+
+ private void onCloseClick() {
+ client.updateVariable(id, "close", true, true);
+ }
+
+ private void onResizeEvent(Event event) {
+ if (resizable && Util.isTouchEventOrLeftMouseButton(event)) {
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEDOWN:
+ case Event.ONTOUCHSTART:
+ if (!isActive()) {
+ bringToFront();
+ }
+ showResizingCurtain();
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "hidden");
+ }
+ resizing = true;
+ startX = Util.getTouchOrMouseClientX(event);
+ startY = Util.getTouchOrMouseClientY(event);
+ origW = getElement().getOffsetWidth();
+ origH = getElement().getOffsetHeight();
+ DOM.setCapture(getElement());
+ event.preventDefault();
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ setSize(event, true);
+ case Event.ONTOUCHCANCEL:
+ DOM.releaseCapture(getElement());
+ case Event.ONLOSECAPTURE:
+ hideResizingCurtain();
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "");
+ }
+ resizing = false;
+ break;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ if (resizing) {
+ centered = false;
+ setSize(event, false);
+ event.preventDefault();
+ }
+ break;
+ default:
+ event.preventDefault();
+ break;
+ }
+ }
+ }
+
+ /**
+ * TODO check if we need to support this with touch based devices.
+ *
+ * Checks if the cursor was inside the browser content area when the event
+ * happened.
+ *
+ * @param event
+ * The event to be checked
+ * @return true, if the cursor is inside the browser content area
+ *
+ * false, otherwise
+ */
+ private boolean cursorInsideBrowserContentArea(Event event) {
+ if (event.getClientX() < 0 || event.getClientY() < 0) {
+ // Outside to the left or above
+ return false;
+ }
+
+ if (event.getClientX() > Window.getClientWidth()
+ || event.getClientY() > Window.getClientHeight()) {
+ // Outside to the right or below
+ return false;
+ }
+
+ return true;
+ }
+
+ private void setSize(Event event, boolean updateVariables) {
+ if (!cursorInsideBrowserContentArea(event)) {
+ // Only drag while cursor is inside the browser client area
+ return;
+ }
+
+ int w = Util.getTouchOrMouseClientX(event) - startX + origW;
+ int minWidth = getMinWidth();
+ if (w < minWidth) {
+ w = minWidth;
+ }
+
+ int h = Util.getTouchOrMouseClientY(event) - startY + origH;
+ int minHeight = getMinHeight();
+ if (h < minHeight) {
+ h = minHeight;
+ }
+
+ 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);
+ }
+
+ if (updateVariables || !resizeLazy) {
+ // Resize has finished or is not lazy
+ updateContentsSize();
+ } else {
+ // Lazy resize - wait for a while before re-rendering contents
+ delayedContentsSizeUpdater.trigger();
+ }
+ }
+
+ private void updateContentsSize() {
+ // Update child widget dimensions
+ if (client != null) {
+ client.handleComponentRelativeSize(layout.getWidget());
+ client.runDescendentsLayout((HasWidgets) layout.getWidget());
+ }
+
+ LayoutManager layoutManager = LayoutManager.get(client);
+ layoutManager.setNeedsMeasure(ConnectorMap.get(client).getConnector(
+ this));
+ layoutManager.layoutNow();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ // Override PopupPanel which sets the width to the contents
+ getElement().getStyle().setProperty("width", width);
+ // Update v-has-width in case undefined window is resized
+ setStyleName("v-has-width", width != null && width.length() > 0);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ // Override PopupPanel which sets the height to the contents
+ getElement().getStyle().setProperty("height", height);
+ // Update v-has-height in case undefined window is resized
+ setStyleName("v-has-height", height != null && height.length() > 0);
+ }
+
+ private void onDragEvent(Event event) {
+ if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ return;
+ }
+
+ switch (DOM.eventGetType(event)) {
+ case Event.ONTOUCHSTART:
+ if (event.getTouches().length() > 1) {
+ return;
+ }
+ case Event.ONMOUSEDOWN:
+ if (!isActive()) {
+ bringToFront();
+ }
+ beginMovingWindow(event);
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ case Event.ONLOSECAPTURE:
+ stopMovingWindow();
+ break;
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ moveWindow(event);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void moveWindow(Event event) {
+ if (dragging) {
+ centered = false;
+ if (cursorInsideBrowserContentArea(event)) {
+ // Only drag while cursor is inside the browser client area
+ final int x = Util.getTouchOrMouseClientX(event) - startX
+ + origX;
+ final int y = Util.getTouchOrMouseClientY(event) - startY
+ + origY;
+ setPopupPosition(x, y);
+ }
+ DOM.eventPreventDefault(event);
+ }
+ }
+
+ private void beginMovingWindow(Event event) {
+ if (draggable) {
+ showDraggingCurtain();
+ dragging = true;
+ startX = Util.getTouchOrMouseClientX(event);
+ startY = Util.getTouchOrMouseClientY(event);
+ origX = DOM.getAbsoluteLeft(getElement());
+ origY = DOM.getAbsoluteTop(getElement());
+ DOM.setCapture(getElement());
+ DOM.eventPreventDefault(event);
+ }
+ }
+
+ private void stopMovingWindow() {
+ dragging = false;
+ hideDraggingCurtain();
+ DOM.releaseCapture(getElement());
+ }
+
+ @Override
+ public boolean onEventPreview(Event event) {
+ if (dragging) {
+ onDragEvent(event);
+ return false;
+ } else if (resizing) {
+ onResizeEvent(event);
+ return false;
+ }
+
+ // TODO This is probably completely unnecessary as the modality curtain
+ // prevents events from reaching other windows and any security check
+ // must be done on the server side and not here.
+ // The code here is also run many times as each VWindow has an event
+ // preview but we cannot check only the current VWindow here (e.g.
+ // if(isTopMost) {...}) because PopupPanel will cause all events that
+ // are not cancelled here and target this window to be consume():d
+ // meaning the event won't be sent to the rest of the preview handlers.
+
+ if (getTopmostWindow().vaadinModality) {
+ // Topmost window is modal. Cancel the event if it targets something
+ // outside that window (except debug console...)
+ if (DOM.getCaptureElement() != null) {
+ // Allow events when capture is set
+ return true;
+ }
+
+ final Element target = event.getEventTarget().cast();
+ if (!DOM.isOrHasChild(getTopmostWindow().getElement(), target)) {
+ // not within the modal window, but let's see if it's in the
+ // debug window
+ Widget w = Util.findWidget(target, null);
+ while (w != null) {
+ if (w instanceof Console) {
+ return true; // allow debug-window clicks
+ } else if (ConnectorMap.get(client).isConnector(w)) {
+ return false;
+ }
+ w = w.getParent();
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void addStyleDependentName(String styleSuffix) {
+ // VWindow's getStyleElement() does not return the same element as
+ // getElement(), so we need to override this.
+ setStyleName(getElement(), getStylePrimaryName() + "-" + styleSuffix,
+ true);
+ }
+
+ @Override
+ public ShortcutActionHandler getShortcutActionHandler() {
+ return shortcutHandler;
+ }
+
+ @Override
+ public void onScroll(ScrollEvent event) {
+ client.updateVariable(id, "scrollTop",
+ contentPanel.getScrollPosition(), false);
+ client.updateVariable(id, "scrollLeft",
+ contentPanel.getHorizontalScrollPosition(), false);
+
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (shortcutHandler != null) {
+ shortcutHandler
+ .handleKeyboardEvent(Event.as(event.getNativeEvent()));
+ return;
+ }
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ if (client.hasEventListeners(this, EventId.BLUR)) {
+ client.updateVariable(id, EventId.BLUR, "", true);
+ }
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(id, EventId.FOCUS, "", true);
+ }
+ }
+
+ @Override
+ public void focus() {
+ contentPanel.focus();
+ }
+
+ public int getMinHeight() {
+ return MIN_CONTENT_AREA_HEIGHT + getDecorationHeight();
+ }
+
+ private int getDecorationHeight() {
+ LayoutManager lm = layout.getLayoutManager();
+ int headerHeight = lm.getOuterHeight(header);
+ int footerHeight = lm.getOuterHeight(footer);
+ return headerHeight + footerHeight;
+ }
+
+ public int getMinWidth() {
+ return MIN_CONTENT_AREA_WIDTH + getDecorationWidth();
+ }
+
+ private int getDecorationWidth() {
+ LayoutManager layoutManager = layout.getLayoutManager();
+ return layoutManager.getOuterWidth(getElement())
+ - contentPanel.getElement().getOffsetWidth();
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java b/client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java
new file mode 100644
index 0000000000..bae4f804fc
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/client/ui/window/WindowConnector.java
@@ -0,0 +1,306 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.client.ui.window;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+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.Widget;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.window.WindowServerRpc;
+import com.vaadin.shared.ui.window.WindowState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.terminal.gwt.client.LayoutManager;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy;
+import com.vaadin.terminal.gwt.client.ui.AbstractComponentContainerConnector;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.PostLayoutListener;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler;
+import com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler.BeforeShortcutActionListener;
+import com.vaadin.terminal.gwt.client.ui.SimpleManagedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.MayScrollChildren;
+
+@Connect(value = com.vaadin.ui.Window.class)
+public class WindowConnector extends AbstractComponentContainerConnector
+ implements Paintable, BeforeShortcutActionListener,
+ SimpleManagedLayout, PostLayoutListener, MayScrollChildren {
+
+ private ClickEventHandler clickEventHandler = new ClickEventHandler(this) {
+ @Override
+ protected void fireClick(NativeEvent event,
+ MouseEventDetails mouseDetails) {
+ rpc.click(mouseDetails);
+ }
+ };
+
+ private WindowServerRpc rpc;
+
+ boolean minWidthChecked = false;
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ };
+
+ @Override
+ protected void init() {
+ super.init();
+ rpc = RpcProxy.create(WindowServerRpc.class, this);
+
+ getLayoutManager().registerDependency(this,
+ getWidget().contentPanel.getElement());
+ getLayoutManager().registerDependency(this, getWidget().header);
+ getLayoutManager().registerDependency(this, getWidget().footer);
+ }
+
+ @Override
+ public void onUnregister() {
+ LayoutManager lm = getLayoutManager();
+ VWindow window = getWidget();
+ lm.unregisterDependency(this, window.contentPanel.getElement());
+ lm.unregisterDependency(this, window.header);
+ lm.unregisterDependency(this, window.footer);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ getWidget().id = getConnectorId();
+ getWidget().client = client;
+
+ // Workaround needed for Testing Tools (GWT generates window DOM
+ // slightly different in different browsers).
+ DOM.setElementProperty(getWidget().closeBox, "id", getConnectorId()
+ + "_window_close");
+
+ if (isRealUpdate(uidl)) {
+ if (getState().isModal() != getWidget().vaadinModality) {
+ getWidget().setVaadinModality(!getWidget().vaadinModality);
+ }
+ if (!getWidget().isAttached()) {
+ getWidget().setVisible(false); // hide until
+ // possible centering
+ getWidget().show();
+ }
+ if (getState().isResizable() != getWidget().resizable) {
+ getWidget().setResizable(getState().isResizable());
+ }
+ getWidget().resizeLazy = getState().isResizeLazy();
+
+ getWidget().setDraggable(getState().isDraggable());
+
+ // Caption must be set before required header size is measured. If
+ // the caption attribute is missing the caption should be cleared.
+ String iconURL = null;
+ if (getState().getIcon() != null) {
+ iconURL = getState().getIcon().getURL();
+ }
+ getWidget().setCaption(getState().getCaption(), iconURL);
+ }
+
+ getWidget().visibilityChangesDisabled = true;
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+ getWidget().visibilityChangesDisabled = false;
+
+ clickEventHandler.handleEventHandlerRegistration();
+
+ getWidget().immediate = getState().isImmediate();
+
+ getWidget().setClosable(!isReadOnly());
+
+ // Initialize the position form UIDL
+ int positionx = getState().getPositionX();
+ int positiony = getState().getPositionY();
+ if (positionx >= 0 || positiony >= 0) {
+ if (positionx < 0) {
+ positionx = 0;
+ }
+ if (positiony < 0) {
+ positiony = 0;
+ }
+ getWidget().setPopupPosition(positionx, positiony);
+ }
+
+ int childIndex = 0;
+
+ // we may have actions
+ for (int i = 0; i < uidl.getChildCount(); i++) {
+ UIDL childUidl = uidl.getChildUIDL(i);
+ if (childUidl.getTag().equals("actions")) {
+ if (getWidget().shortcutHandler == null) {
+ getWidget().shortcutHandler = new ShortcutActionHandler(
+ getConnectorId(), client);
+ }
+ getWidget().shortcutHandler.updateActionMap(childUidl);
+ }
+
+ }
+
+ // setting scrollposition must happen after children is rendered
+ getWidget().contentPanel.setScrollPosition(getState().getScrollTop());
+ getWidget().contentPanel.setHorizontalScrollPosition(getState()
+ .getScrollLeft());
+
+ // Center this window on screen if requested
+ // This had to be here because we might not know the content size before
+ // everything is painted into the window
+
+ // centered is this is unset on move/resize
+ getWidget().centered = getState().isCentered();
+ getWidget().setVisible(true);
+
+ // ensure window is not larger than browser window
+ if (getWidget().getOffsetWidth() > Window.getClientWidth()) {
+ getWidget().setWidth(Window.getClientWidth() + "px");
+ }
+ if (getWidget().getOffsetHeight() > Window.getClientHeight()) {
+ getWidget().setHeight(Window.getClientHeight() + "px");
+ }
+
+ if (uidl.hasAttribute("bringToFront")) {
+ /*
+ * Focus as a side-effect. Will be overridden by
+ * ApplicationConnection if another component was focused by the
+ * server side.
+ */
+ getWidget().contentPanel.focus();
+ getWidget().bringToFrontSequence = uidl
+ .getIntAttribute("bringToFront");
+ VWindow.deferOrdering();
+ }
+ }
+
+ @Override
+ public void updateCaption(ComponentConnector component) {
+ // NOP, window has own caption, layout caption not rendered
+ }
+
+ @Override
+ public void onBeforeShortcutAction(Event e) {
+ // NOP, nothing to update just avoid workaround ( causes excess
+ // blur/focus )
+ }
+
+ @Override
+ public VWindow getWidget() {
+ return (VWindow) super.getWidget();
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(ConnectorHierarchyChangeEvent event) {
+ super.onConnectorHierarchyChange(event);
+
+ // We always have 1 child, unless the child is hidden
+ Widget newChildWidget = null;
+ ComponentConnector newChild = null;
+ if (getChildComponents().size() == 1) {
+ newChild = getChildComponents().get(0);
+ newChildWidget = newChild.getWidget();
+ }
+
+ getWidget().layout = newChild;
+ getWidget().contentPanel.setWidget(newChildWidget);
+ }
+
+ @Override
+ public void layout() {
+ LayoutManager lm = getLayoutManager();
+ VWindow window = getWidget();
+ ComponentConnector layout = window.layout;
+ Element contentElement = window.contentPanel.getElement();
+
+ if (!minWidthChecked) {
+ boolean needsMinWidth = !isUndefinedWidth()
+ || layout.isRelativeWidth();
+ int minWidth = window.getMinWidth();
+ if (needsMinWidth && lm.getInnerWidth(contentElement) < minWidth) {
+ minWidthChecked = true;
+ // Use minimum width if less than a certain size
+ window.setWidth(minWidth + "px");
+ }
+ minWidthChecked = true;
+ }
+
+ boolean needsMinHeight = !isUndefinedHeight()
+ || layout.isRelativeHeight();
+ int minHeight = window.getMinHeight();
+ if (needsMinHeight && lm.getInnerHeight(contentElement) < minHeight) {
+ // Use minimum height if less than a certain size
+ window.setHeight(minHeight + "px");
+ }
+
+ Style contentStyle = window.contents.getStyle();
+
+ int headerHeight = lm.getOuterHeight(window.header);
+ contentStyle.setPaddingTop(headerHeight, Unit.PX);
+ contentStyle.setMarginTop(-headerHeight, Unit.PX);
+
+ int footerHeight = lm.getOuterHeight(window.footer);
+ contentStyle.setPaddingBottom(footerHeight, Unit.PX);
+ contentStyle.setMarginBottom(-footerHeight, Unit.PX);
+
+ /*
+ * Must set absolute position if the child has relative height and
+ * there's a chance of horizontal scrolling as some browsers will
+ * otherwise not take the scrollbar into account when calculating the
+ * height.
+ */
+ Element layoutElement = layout.getWidget().getElement();
+ Style childStyle = layoutElement.getStyle();
+ if (layout.isRelativeHeight() && !BrowserInfo.get().isIE9()) {
+ childStyle.setPosition(Position.ABSOLUTE);
+
+ Style wrapperStyle = contentElement.getStyle();
+ if (window.getElement().getStyle().getWidth().length() == 0
+ && !layout.isRelativeWidth()) {
+ /*
+ * Need to lock width to make undefined width work even with
+ * absolute positioning
+ */
+ int contentWidth = lm.getOuterWidth(layoutElement);
+ wrapperStyle.setWidth(contentWidth, Unit.PX);
+ } else {
+ wrapperStyle.clearWidth();
+ }
+ } else {
+ childStyle.clearPosition();
+ }
+ }
+
+ @Override
+ public void postLayout() {
+ minWidthChecked = false;
+ VWindow window = getWidget();
+ if (window.centered) {
+ window.center();
+ }
+ window.sizeOrPositionUpdated();
+ }
+
+ @Override
+ public WindowState getState() {
+ return (WindowState) super.getState();
+ }
+
+ /**
+ * Gives the WindowConnector an order number. As a side effect, moves the
+ * window according to its order number so the windows are stacked. This
+ * method should be called for each window in the order they should appear.
+ */
+ public void setWindowOrderAndPosition() {
+ getWidget().setWindowOrderAndPosition();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gif b/client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gif
new file mode 100644
index 0000000000..3776af0784
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/public/ie6pngfix/blank.gif
Binary files differ
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/AbstractConnectorClassBasedFactoryGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/AbstractConnectorClassBasedFactoryGenerator.java
new file mode 100644
index 0000000000..3a13fceece
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/AbstractConnectorClassBasedFactoryGenerator.java
@@ -0,0 +1,145 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.util.Date;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.ui.ConnectorClassBasedFactory;
+import com.vaadin.terminal.gwt.client.ui.ConnectorClassBasedFactory.Creator;
+
+/**
+ * GWT generator that creates a lookup method for
+ * {@link ConnectorClassBasedFactory} instances.
+ *
+ * @since 7.0
+ */
+public abstract class AbstractConnectorClassBasedFactoryGenerator extends
+ Generator {
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext context,
+ String typeName) throws UnableToCompleteException {
+
+ try {
+ // get classType and save instance variables
+ return generateConnectorClassBasedFactory(typeName, logger, context);
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR, typeName + " creation failed", e);
+ throw new UnableToCompleteException();
+ }
+ }
+
+ private String generateConnectorClassBasedFactory(String typeName,
+ TreeLogger logger, GeneratorContext context)
+ throws NotFoundException {
+ TypeOracle typeOracle = context.getTypeOracle();
+
+ JClassType classType = typeOracle.getType(typeName);
+ String superName = classType.getSimpleSourceName();
+ String packageName = classType.getPackage().getName();
+ String className = superName + "Impl";
+
+ // get print writer that receives the source code
+ PrintWriter printWriter = null;
+ printWriter = context.tryCreate(logger, packageName, className);
+ // print writer if null, source code has ALREADY been generated
+ if (printWriter == null) {
+ return packageName + "." + className;
+ }
+
+ Date date = new Date();
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(packageName, className);
+ composer.addImport(GWT.class.getName());
+ composer.addImport(Creator.class.getCanonicalName());
+ composer.setSuperclass(superName);
+
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+ sourceWriter.indent();
+
+ // public ConnectorStateFactoryImpl() {
+ sourceWriter.println("public " + className + "() {");
+ sourceWriter.indent();
+
+ JClassType serverConnectorType = typeOracle.getType(getConnectorType()
+ .getCanonicalName());
+ for (JClassType connector : serverConnectorType.getSubtypes()) {
+ // addCreator(TextAreaConnector.class, new Creator<SharedState>() {
+ if (connector.isInterface() != null || connector.isAbstract()) {
+ continue;
+ }
+
+ JClassType targetType = getTargetType(connector);
+ if (targetType.isAbstract()) {
+ continue;
+ }
+
+ sourceWriter.println("addCreator("
+ + connector.getQualifiedSourceName()
+ + ".class, new Creator<"
+ + targetType.getQualifiedSourceName() + ">() {");
+ // public SharedState create() {
+ sourceWriter.println("public "
+ + targetType.getQualifiedSourceName() + " create() {");
+ // return GWT.create(TextAreaState.class);
+ sourceWriter.println("return GWT.create("
+ + targetType.getQualifiedSourceName() + ".class);");
+ // }
+ sourceWriter.println("}");
+ // });
+ sourceWriter.println("});");
+ }
+
+ // End of constructor
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ // close generated class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(Type.INFO,
+ "Done. (" + (new Date().getTime() - date.getTime()) / 1000
+ + "seconds)");
+ return packageName + "." + className;
+
+ }
+
+ protected abstract Class<? extends ServerConnector> getConnectorType();
+
+ protected abstract JClassType getTargetType(JClassType connectorType);
+
+ protected JClassType getGetterReturnType(JClassType connector,
+ String getterName) {
+ try {
+ JMethod getMethod = connector.getMethod(getterName, new JType[] {});
+ return (JClassType) getMethod.getReturnType();
+ } catch (NotFoundException e) {
+ return getGetterReturnType(connector.getSuperclass(), getterName);
+ }
+
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/AcceptCriteriaFactoryGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/AcceptCriteriaFactoryGenerator.java
new file mode 100644
index 0000000000..e5e2ee1f2c
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/AcceptCriteriaFactoryGenerator.java
@@ -0,0 +1,127 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.util.Date;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCriterion;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCriterionFactory;
+
+/**
+ * GWT generator to build {@link VAcceptCriterionFactory} implementation
+ * dynamically based on {@link AcceptCriterion} annotations available in
+ * classpath.
+ *
+ */
+public class AcceptCriteriaFactoryGenerator extends Generator {
+
+ private String packageName;
+ private String className;
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext context,
+ String typeName) throws UnableToCompleteException {
+
+ try {
+ TypeOracle typeOracle = context.getTypeOracle();
+
+ // get classType and save instance variables
+ JClassType classType = typeOracle.getType(typeName);
+ packageName = classType.getPackage().getName();
+ className = classType.getSimpleSourceName() + "Impl";
+ // Generate class source code
+ generateClass(logger, context);
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR,
+ "Accept criterion factory creation failed", e);
+ }
+ // return the fully qualifed name of the class generated
+ return packageName + "." + className;
+ }
+
+ /**
+ * Generate source code for WidgetMapImpl
+ *
+ * @param logger
+ * Logger object
+ * @param context
+ * Generator context
+ */
+ private void generateClass(TreeLogger logger, GeneratorContext context) {
+ // get print writer that receives the source code
+ PrintWriter printWriter = null;
+ printWriter = context.tryCreate(logger, packageName, className);
+ // print writer if null, source code has ALREADY been generated,
+ // return (WidgetMap is equal to all permutations atm)
+ if (printWriter == null) {
+ return;
+ }
+ logger.log(Type.INFO, "Detecting available criteria ...");
+ Date date = new Date();
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(packageName, className);
+ composer.addImport("com.google.gwt.core.client.GWT");
+ composer.setSuperclass("com.vaadin.terminal.gwt.client.ui.dd.VAcceptCriterionFactory");
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+
+ // generator constructor source code
+ generateInstantiatorMethod(sourceWriter, context, logger);
+ // close generated class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(Type.INFO,
+ "Done. (" + (new Date().getTime() - date.getTime()) / 1000
+ + "seconds)");
+
+ }
+
+ private void generateInstantiatorMethod(SourceWriter sourceWriter,
+ GeneratorContext context, TreeLogger logger) {
+
+ sourceWriter.println("public VAcceptCriterion get(String name) {");
+ sourceWriter.indent();
+
+ sourceWriter.println("name = name.intern();");
+
+ JClassType criteriaType = context.getTypeOracle().findType(
+ VAcceptCriterion.class.getName());
+ for (JClassType clientClass : criteriaType.getSubtypes()) {
+ AcceptCriterion annotation = clientClass
+ .getAnnotation(AcceptCriterion.class);
+ if (annotation != null) {
+ String clientClassName = clientClass.getQualifiedSourceName();
+ Class<?> serverClass = clientClass.getAnnotation(
+ AcceptCriterion.class).value();
+ String serverClassName = serverClass.getCanonicalName();
+ logger.log(Type.INFO, "creating mapping for " + serverClassName);
+ sourceWriter.print("if (\"");
+ sourceWriter.print(serverClassName);
+ sourceWriter.print("\" == name) return GWT.create(");
+ sourceWriter.print(clientClassName);
+ sourceWriter.println(".class );");
+ sourceWriter.print("else ");
+ }
+ }
+
+ sourceWriter.println("return null;");
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java
new file mode 100644
index 0000000000..6ee30183c1
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java
@@ -0,0 +1,462 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.net.JarURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility class to collect widgetset related information from classpath.
+ * Utility will seek all directories from classpaths, and jar files having
+ * "Vaadin-Widgetsets" key in their manifest file.
+ * <p>
+ * Used by WidgetMapGenerator and ide tools to implement some monkey coding for
+ * you.
+ * <p>
+ * Developer notice: If you end up reading this comment, I guess you have faced
+ * a sluggish performance of widget compilation or unreliable detection of
+ * components in your classpaths. The thing you might be able to do is to use
+ * annotation processing tool like apt to generate the needed information. Then
+ * either use that information in {@link WidgetMapGenerator} or create the
+ * appropriate monkey code for gwt directly in annotation processor and get rid
+ * of {@link WidgetMapGenerator}. Using annotation processor might be a good
+ * idea when dropping Java 1.5 support (integrated to javac in 6).
+ *
+ */
+public class ClassPathExplorer {
+
+ private static final String VAADIN_ADDON_VERSION_ATTRIBUTE = "Vaadin-Package-Version";
+
+ /**
+ * File filter that only accepts directories.
+ */
+ private final static FileFilter DIRECTORIES_ONLY = new FileFilter() {
+ @Override
+ public boolean accept(File f) {
+ if (f.exists() && f.isDirectory()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ /**
+ * Raw class path entries as given in the java class path string. Only
+ * entries that could include widgets/widgetsets are listed (primarily
+ * directories, Vaadin JARs and add-on JARs).
+ */
+ private static List<String> rawClasspathEntries = getRawClasspathEntries();
+
+ /**
+ * Map from identifiers (either a package name preceded by the path and a
+ * slash, or a URL for a JAR file) to the corresponding URLs. This is
+ * constructed from the class path.
+ */
+ private static Map<String, URL> classpathLocations = getClasspathLocations(rawClasspathEntries);
+
+ /**
+ * No instantiation from outside, callable methods are static.
+ */
+ private ClassPathExplorer() {
+ }
+
+ /**
+ * Finds the names and locations of widgetsets available on the class path.
+ *
+ * @return map from widgetset classname to widgetset location URL
+ */
+ public static Map<String, URL> getAvailableWidgetSets() {
+ long start = System.currentTimeMillis();
+ Map<String, URL> widgetsets = new HashMap<String, URL>();
+ Set<String> keySet = classpathLocations.keySet();
+ for (String location : keySet) {
+ searchForWidgetSets(location, widgetsets);
+ }
+ long end = System.currentTimeMillis();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Widgetsets found from classpath:\n");
+ for (String ws : widgetsets.keySet()) {
+ sb.append("\t");
+ sb.append(ws);
+ sb.append(" in ");
+ sb.append(widgetsets.get(ws));
+ sb.append("\n");
+ }
+ final Logger logger = getLogger();
+ logger.info(sb.toString());
+ logger.info("Search took " + (end - start) + "ms");
+ return widgetsets;
+ }
+
+ /**
+ * Finds all GWT modules / Vaadin widgetsets in a valid location.
+ *
+ * If the location is a directory, all GWT modules (files with the
+ * ".gwt.xml" extension) are added to widgetsets.
+ *
+ * If the location is a JAR file, the comma-separated values of the
+ * "Vaadin-Widgetsets" attribute in its manifest are added to widgetsets.
+ *
+ * @param locationString
+ * an entry in {@link #classpathLocations}
+ * @param widgetsets
+ * a map from widgetset name (including package, with dots as
+ * separators) to a URL (see {@link #classpathLocations}) - new
+ * entries are added to this map
+ */
+ private static void searchForWidgetSets(String locationString,
+ Map<String, URL> widgetsets) {
+
+ URL location = classpathLocations.get(locationString);
+ File directory = new File(location.getFile());
+
+ if (directory.exists() && !directory.isHidden()) {
+ // Get the list of the files contained in the directory
+ String[] files = directory.list();
+ for (int i = 0; i < files.length; i++) {
+ // we are only interested in .gwt.xml files
+ if (!files[i].endsWith(".gwt.xml")) {
+ continue;
+ }
+
+ // remove the .gwt.xml extension
+ String classname = files[i].substring(0, files[i].length() - 8);
+ String packageName = locationString.substring(locationString
+ .lastIndexOf("/") + 1);
+ classname = packageName + "." + classname;
+
+ if (!WidgetSetBuilder.isWidgetset(classname)) {
+ // Only return widgetsets and not GWT modules to avoid
+ // comparing modules and widgetsets
+ continue;
+ }
+
+ if (!widgetsets.containsKey(classname)) {
+ String packagePath = packageName.replaceAll("\\.", "/");
+ String basePath = location.getFile().replaceAll(
+ "/" + packagePath + "$", "");
+ try {
+ URL url = new URL(location.getProtocol(),
+ location.getHost(), location.getPort(),
+ basePath);
+ widgetsets.put(classname, url);
+ } catch (MalformedURLException e) {
+ // should never happen as based on an existing URL,
+ // only changing end of file name/path part
+ getLogger().log(Level.SEVERE,
+ "Error locating the widgetset " + classname, e);
+ }
+ }
+ }
+ } else {
+
+ try {
+ // check files in jar file, entries will list all directories
+ // and files in jar
+
+ URLConnection openConnection = location.openConnection();
+ if (openConnection instanceof JarURLConnection) {
+ JarURLConnection conn = (JarURLConnection) openConnection;
+
+ JarFile jarFile = conn.getJarFile();
+
+ Manifest manifest = jarFile.getManifest();
+ if (manifest == null) {
+ // No manifest so this is not a Vaadin Add-on
+ return;
+ }
+ String value = manifest.getMainAttributes().getValue(
+ "Vaadin-Widgetsets");
+ if (value != null) {
+ String[] widgetsetNames = value.split(",");
+ for (int i = 0; i < widgetsetNames.length; i++) {
+ String widgetsetname = widgetsetNames[i].trim()
+ .intern();
+ if (!widgetsetname.equals("")) {
+ widgetsets.put(widgetsetname, location);
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ getLogger().log(Level.WARNING, "Error parsing jar file", e);
+ }
+
+ }
+ }
+
+ /**
+ * Splits the current class path into entries, and filters them accepting
+ * directories, Vaadin add-on JARs with widgetsets and Vaadin JARs.
+ *
+ * Some other non-JAR entries may also be included in the result.
+ *
+ * @return filtered list of class path entries
+ */
+ private final static List<String> getRawClasspathEntries() {
+ // try to keep the order of the classpath
+ List<String> locations = new ArrayList<String>();
+
+ String pathSep = System.getProperty("path.separator");
+ String classpath = System.getProperty("java.class.path");
+
+ if (classpath.startsWith("\"")) {
+ classpath = classpath.substring(1);
+ }
+ if (classpath.endsWith("\"")) {
+ classpath = classpath.substring(0, classpath.length() - 1);
+ }
+
+ getLogger().fine("Classpath: " + classpath);
+
+ String[] split = classpath.split(pathSep);
+ for (int i = 0; i < split.length; i++) {
+ String classpathEntry = split[i];
+ if (acceptClassPathEntry(classpathEntry)) {
+ locations.add(classpathEntry);
+ }
+ }
+
+ return locations;
+ }
+
+ /**
+ * Determine every URL location defined by the current classpath, and it's
+ * associated package name.
+ *
+ * See {@link #classpathLocations} for information on output format.
+ *
+ * @param rawClasspathEntries
+ * raw class path entries as split from the Java class path
+ * string
+ * @return map of classpath locations, see {@link #classpathLocations}
+ */
+ private final static Map<String, URL> getClasspathLocations(
+ List<String> rawClasspathEntries) {
+ long start = System.currentTimeMillis();
+ // try to keep the order of the classpath
+ Map<String, URL> locations = new LinkedHashMap<String, URL>();
+ for (String classpathEntry : rawClasspathEntries) {
+ File file = new File(classpathEntry);
+ include(null, file, locations);
+ }
+ long end = System.currentTimeMillis();
+ Logger logger = getLogger();
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("getClassPathLocations took " + (end - start) + "ms");
+ }
+ return locations;
+ }
+
+ /**
+ * Checks a class path entry to see whether it can contain widgets and
+ * widgetsets.
+ *
+ * All directories are automatically accepted. JARs are accepted if they
+ * have the "Vaadin-Widgetsets" attribute in their manifest or the JAR file
+ * name contains "vaadin-" or ".vaadin.".
+ *
+ * Also other non-JAR entries may be accepted, the caller should be prepared
+ * to handle them.
+ *
+ * @param classpathEntry
+ * class path entry string as given in the Java class path
+ * @return true if the entry should be considered when looking for widgets
+ * or widgetsets
+ */
+ private static boolean acceptClassPathEntry(String classpathEntry) {
+ if (!classpathEntry.endsWith(".jar")) {
+ // accept all non jars (practically directories)
+ return true;
+ } else {
+ // accepts jars that comply with vaadin-component packaging
+ // convention (.vaadin. or vaadin- as distribution packages),
+ if (classpathEntry.contains("vaadin-")
+ || classpathEntry.contains(".vaadin.")) {
+ return true;
+ } else {
+ URL url;
+ try {
+ url = new URL("file:"
+ + new File(classpathEntry).getCanonicalPath());
+ url = new URL("jar:" + url.toExternalForm() + "!/");
+ JarURLConnection conn = (JarURLConnection) url
+ .openConnection();
+ getLogger().fine(url.toString());
+ JarFile jarFile = conn.getJarFile();
+ Manifest manifest = jarFile.getManifest();
+ if (manifest != null) {
+ Attributes mainAttributes = manifest
+ .getMainAttributes();
+ if (mainAttributes.getValue("Vaadin-Widgetsets") != null) {
+ return true;
+ }
+ }
+ } catch (MalformedURLException e) {
+ getLogger().log(Level.FINEST, "Failed to inspect JAR file",
+ e);
+ } catch (IOException e) {
+ getLogger().log(Level.FINEST, "Failed to inspect JAR file",
+ e);
+ }
+
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Recursively add subdirectories and jar files to locations - see
+ * {@link #classpathLocations}.
+ *
+ * @param name
+ * @param file
+ * @param locations
+ */
+ private final static void include(String name, File file,
+ Map<String, URL> locations) {
+ if (!file.exists()) {
+ return;
+ }
+ if (!file.isDirectory()) {
+ // could be a JAR file
+ includeJar(file, locations);
+ return;
+ }
+
+ if (file.isHidden() || file.getPath().contains(File.separator + ".")) {
+ return;
+ }
+
+ if (name == null) {
+ name = "";
+ } else {
+ name += ".";
+ }
+
+ // add all directories recursively
+ File[] dirs = file.listFiles(DIRECTORIES_ONLY);
+ for (int i = 0; i < dirs.length; i++) {
+ try {
+ // add the present directory
+ if (!dirs[i].isHidden()
+ && !dirs[i].getPath().contains(File.separator + ".")) {
+ String key = dirs[i].getCanonicalPath() + "/" + name
+ + dirs[i].getName();
+ locations.put(key,
+ new URL("file://" + dirs[i].getCanonicalPath()));
+ }
+ } catch (Exception ioe) {
+ return;
+ }
+ include(name + dirs[i].getName(), dirs[i], locations);
+ }
+ }
+
+ /**
+ * Add a jar file to locations - see {@link #classpathLocations}.
+ *
+ * @param name
+ * @param locations
+ */
+ private static void includeJar(File file, Map<String, URL> locations) {
+ try {
+ URL url = new URL("file:" + file.getCanonicalPath());
+ url = new URL("jar:" + url.toExternalForm() + "!/");
+ JarURLConnection conn = (JarURLConnection) url.openConnection();
+ JarFile jarFile = conn.getJarFile();
+ if (jarFile != null) {
+ // the key does not matter here as long as it is unique
+ locations.put(url.toString(), url);
+ }
+ } catch (Exception e) {
+ // e.printStackTrace();
+ return;
+ }
+
+ }
+
+ /**
+ * Find and return the default source directory where to create new
+ * widgetsets.
+ *
+ * Return the first directory (not a JAR file etc.) on the classpath by
+ * default.
+ *
+ * TODO this could be done better...
+ *
+ * @return URL
+ */
+ public static URL getDefaultSourceDirectory() {
+
+ final Logger logger = getLogger();
+
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("classpathLocations values:");
+ ArrayList<String> locations = new ArrayList<String>(
+ classpathLocations.keySet());
+ for (String location : locations) {
+ logger.fine(String.valueOf(classpathLocations.get(location)));
+ }
+ }
+
+ Iterator<String> it = rawClasspathEntries.iterator();
+ while (it.hasNext()) {
+ String entry = it.next();
+
+ File directory = new File(entry);
+ if (directory.exists() && !directory.isHidden()
+ && directory.isDirectory()) {
+ try {
+ return new URL("file://" + directory.getCanonicalPath());
+ } catch (MalformedURLException e) {
+ logger.log(Level.FINEST, "Ignoring exception", e);
+ // ignore: continue to the next classpath entry
+ } catch (IOException e) {
+ logger.log(Level.FINEST, "Ignoring exception", e);
+ // ignore: continue to the next classpath entry
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Test method for helper tool
+ */
+ public static void main(String[] args) {
+ getLogger().info("Searching available widgetsets...");
+
+ Map<String, URL> availableWidgetSets = ClassPathExplorer
+ .getAvailableWidgetSets();
+ for (String string : availableWidgetSets.keySet()) {
+
+ getLogger().info(string + " in " + availableWidgetSets.get(string));
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(ClassPathExplorer.class.getName());
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorStateFactoryGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorStateFactoryGenerator.java
new file mode 100644
index 0000000000..33406ef85f
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorStateFactoryGenerator.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * GWT generator that creates a SharedState class for a given Connector class,
+ * based on the return type of getState()
+ *
+ * @since 7.0
+ */
+public class ConnectorStateFactoryGenerator extends
+ AbstractConnectorClassBasedFactoryGenerator {
+
+ @Override
+ protected JClassType getTargetType(JClassType connectorType) {
+ return getGetterReturnType(connectorType, "getState");
+ }
+
+ @Override
+ protected Class<? extends ServerConnector> getConnectorType() {
+ return ServerConnector.class;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorWidgetFactoryGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorWidgetFactoryGenerator.java
new file mode 100644
index 0000000000..55a2857ce0
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/ConnectorWidgetFactoryGenerator.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * GWT generator that creates a Widget class for a given Connector class, based
+ * on the return type of getWidget()
+ *
+ * @since 7.0
+ */
+public class ConnectorWidgetFactoryGenerator extends
+ AbstractConnectorClassBasedFactoryGenerator {
+ @Override
+ protected JClassType getTargetType(JClassType connectorType) {
+ return getGetterReturnType(connectorType, "getWidget");
+ }
+
+ @Override
+ protected Class<? extends ServerConnector> getConnectorType() {
+ return ComponentConnector.class;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/CustomWidgetMapGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/CustomWidgetMapGenerator.java
new file mode 100644
index 0000000000..89045c63b2
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/CustomWidgetMapGenerator.java
@@ -0,0 +1,84 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * An abstract helper class that can be used to easily build a widgetset with
+ * customized load styles for each components. In three abstract methods one can
+ * override the default values given in {@link Connect} annotations.
+ *
+ * @see WidgetMapGenerator
+ *
+ */
+public abstract class CustomWidgetMapGenerator extends WidgetMapGenerator {
+
+ private Collection<Class<? extends ComponentConnector>> eagerPaintables = new HashSet<Class<? extends ComponentConnector>>();
+ private Collection<Class<? extends ComponentConnector>> lazyPaintables = new HashSet<Class<? extends ComponentConnector>>();
+ private Collection<Class<? extends ComponentConnector>> deferredPaintables = new HashSet<Class<? extends ComponentConnector>>();
+
+ @Override
+ protected LoadStyle getLoadStyle(Class<? extends ServerConnector> connector) {
+ if (eagerPaintables == null) {
+ init();
+ }
+ if (eagerPaintables.contains(connector)) {
+ return LoadStyle.EAGER;
+ }
+ if (lazyPaintables.contains(connector)) {
+ return LoadStyle.LAZY;
+ }
+ if (deferredPaintables.contains(connector)) {
+ return LoadStyle.DEFERRED;
+ }
+ return super.getLoadStyle(connector);
+ }
+
+ private void init() {
+ Class<? extends ComponentConnector>[] eagerComponents = getEagerComponents();
+ if (eagerComponents != null) {
+ for (Class<? extends ComponentConnector> class1 : eagerComponents) {
+ eagerPaintables.add(class1);
+ }
+ }
+ Class<? extends ComponentConnector>[] lazyComponents = getEagerComponents();
+ if (lazyComponents != null) {
+ for (Class<? extends ComponentConnector> class1 : lazyComponents) {
+ lazyPaintables.add(class1);
+ }
+ }
+ Class<? extends ComponentConnector>[] deferredComponents = getEagerComponents();
+ if (deferredComponents != null) {
+ for (Class<? extends ComponentConnector> class1 : deferredComponents) {
+ deferredPaintables.add(class1);
+ }
+ }
+ }
+
+ /**
+ * @return an array of components whose load style should be overridden to
+ * {@link LoadStyle#EAGER}
+ */
+ protected abstract Class<? extends ComponentConnector>[] getEagerComponents();
+
+ /**
+ * @return an array of components whose load style should be overridden to
+ * {@link LoadStyle#LAZY}
+ */
+ protected abstract Class<? extends ComponentConnector>[] getLazyComponents();
+
+ /**
+ * @return an array of components whose load style should be overridden to
+ * {@link LoadStyle#DEFERRED}
+ */
+ protected abstract Class<? extends ComponentConnector>[] getDeferredComponents();
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/EagerWidgetMapGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/EagerWidgetMapGenerator.java
new file mode 100644
index 0000000000..4ff0592ede
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/EagerWidgetMapGenerator.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * WidgetMap generator that builds a widgetset that packs all included widgets
+ * into a single JavaScript file loaded at application initialization. Initially
+ * loaded data will be relatively large, but minimal amount of server requests
+ * will be done.
+ * <p>
+ * This is the default generator in version 6.4 and produces similar type of
+ * widgetset as in previous versions of Vaadin. To activate "code splitting",
+ * use the {@link WidgetMapGenerator} instead, that loads most components
+ * deferred.
+ *
+ * @see WidgetMapGenerator
+ *
+ */
+public class EagerWidgetMapGenerator extends WidgetMapGenerator {
+
+ @Override
+ protected LoadStyle getLoadStyle(Class<? extends ServerConnector> connector) {
+ return LoadStyle.EAGER;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/GeneratedRpcMethodProviderGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/GeneratedRpcMethodProviderGenerator.java
new file mode 100644
index 0000000000..e11a12a3b5
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/GeneratedRpcMethodProviderGenerator.java
@@ -0,0 +1,211 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.terminal.gwt.client.communication.GeneratedRpcMethodProvider;
+import com.vaadin.terminal.gwt.client.communication.RpcManager;
+import com.vaadin.terminal.gwt.client.communication.RpcMethod;
+
+/**
+ * GWT generator that creates an implementation for {@link RpcManager} on the
+ * client side classes for executing RPC calls received from the the server.
+ *
+ * @since 7.0
+ */
+public class GeneratedRpcMethodProviderGenerator extends Generator {
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext context,
+ String typeName) throws UnableToCompleteException {
+
+ String packageName = null;
+ String className = null;
+ try {
+ TypeOracle typeOracle = context.getTypeOracle();
+
+ // get classType and save instance variables
+ JClassType classType = typeOracle.getType(typeName);
+ packageName = classType.getPackage().getName();
+ className = classType.getSimpleSourceName() + "Impl";
+ // Generate class source code for SerializerMapImpl
+ generateClass(logger, context, packageName, className);
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR,
+ "SerializerMapGenerator creation failed", e);
+ }
+ // return the fully qualifed name of the class generated
+ return packageName + "." + className;
+ }
+
+ /**
+ * Generate source code for RpcManagerImpl
+ *
+ * @param logger
+ * Logger object
+ * @param context
+ * Generator context
+ * @param packageName
+ * package name for the class to generate
+ * @param className
+ * class name for the class to generate
+ */
+ private void generateClass(TreeLogger logger, GeneratorContext context,
+ String packageName, String className) {
+ // get print writer that receives the source code
+ PrintWriter printWriter = null;
+ printWriter = context.tryCreate(logger, packageName, className);
+ // print writer if null, source code has ALREADY been generated
+ if (printWriter == null) {
+ return;
+ }
+ logger.log(Type.INFO,
+ "Detecting server to client RPC interface types...");
+ Date date = new Date();
+ TypeOracle typeOracle = context.getTypeOracle();
+ JClassType serverToClientRpcType = typeOracle.findType(ClientRpc.class
+ .getName());
+ JClassType[] rpcInterfaceSubtypes = serverToClientRpcType.getSubtypes();
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(packageName, className);
+ composer.addImport("com.google.gwt.core.client.GWT");
+ composer.addImport(RpcMethod.class.getName());
+ composer.addImport(ClientRpc.class.getName());
+ composer.addImport(com.vaadin.terminal.gwt.client.communication.Type.class
+ .getName());
+ composer.addImplementedInterface(GeneratedRpcMethodProvider.class
+ .getName());
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+ sourceWriter.indent();
+
+ List<JMethod> rpcMethods = new ArrayList<JMethod>();
+
+ sourceWriter
+ .println("public java.util.Collection<RpcMethod> getGeneratedRpcMethods() {");
+ sourceWriter.indent();
+
+ sourceWriter
+ .println("java.util.ArrayList<RpcMethod> list = new java.util.ArrayList<RpcMethod>();");
+
+ // iterate over RPC interfaces and create helper methods for each
+ // interface
+ for (JClassType type : rpcInterfaceSubtypes) {
+ if (null == type.isInterface()) {
+ // only interested in interfaces here, not implementations
+ continue;
+ }
+
+ // loop over the methods of the interface and its superinterfaces
+ // methods
+ for (JClassType currentType : type.getFlattenedSupertypeHierarchy()) {
+ for (JMethod method : currentType.getMethods()) {
+
+ // RpcMethod(String interfaceName, String methodName,
+ // Type... parameterTypes)
+ sourceWriter.print("list.add(new RpcMethod(\""
+ + type.getQualifiedSourceName() + "\", \""
+ + method.getName() + "\"");
+ JType[] parameterTypes = method.getParameterTypes();
+ for (JType parameter : parameterTypes) {
+ sourceWriter.print(", ");
+ writeTypeCreator(sourceWriter, parameter);
+ }
+ sourceWriter.println(") {");
+ sourceWriter.indent();
+
+ sourceWriter
+ .println("public void applyInvocation(ClientRpc target, Object... parameters) {");
+ sourceWriter.indent();
+
+ sourceWriter.print("((" + type.getQualifiedSourceName()
+ + ")target)." + method.getName() + "(");
+ for (int i = 0; i < parameterTypes.length; i++) {
+ JType parameterType = parameterTypes[i];
+ if (i != 0) {
+ sourceWriter.print(", ");
+ }
+ String parameterTypeName = getBoxedTypeName(parameterType);
+ sourceWriter.print("(" + parameterTypeName
+ + ") parameters[" + i + "]");
+ }
+ sourceWriter.println(");");
+
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ sourceWriter.outdent();
+ sourceWriter.println("});");
+ }
+ }
+ }
+
+ sourceWriter.println("return list;");
+
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ sourceWriter.println();
+
+ // close generated class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(Type.INFO,
+ "Done. (" + (new Date().getTime() - date.getTime()) / 1000
+ + "seconds)");
+
+ }
+
+ public static void writeTypeCreator(SourceWriter sourceWriter, JType type) {
+ String typeName = getBoxedTypeName(type);
+ sourceWriter.print("new Type(\"" + typeName + "\", ");
+ JParameterizedType parameterized = type.isParameterized();
+ if (parameterized != null) {
+ sourceWriter.print("new Type[] {");
+ JClassType[] typeArgs = parameterized.getTypeArgs();
+ for (JClassType jClassType : typeArgs) {
+ writeTypeCreator(sourceWriter, jClassType);
+ sourceWriter.print(", ");
+ }
+ sourceWriter.print("}");
+ } else {
+ sourceWriter.print("null");
+ }
+ sourceWriter.print(")");
+ }
+
+ public static String getBoxedTypeName(JType type) {
+ if (type.isPrimitive() != null) {
+ // Used boxed types for primitives
+ return type.isPrimitive().getQualifiedBoxedSourceName();
+ } else {
+ return type.getErasedType().getQualifiedSourceName();
+ }
+ }
+
+ private String getInvokeMethodName(JClassType type) {
+ return "invoke" + type.getQualifiedSourceName().replaceAll("\\.", "_");
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/LazyWidgetMapGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/LazyWidgetMapGenerator.java
new file mode 100644
index 0000000000..28f3dab482
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/LazyWidgetMapGenerator.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+
+/**
+ * WidgetMap generator that builds a widgetset that optimizes the transferred
+ * data. Widgets are loaded only when used if the widgetset is built with this
+ * generator.
+ *
+ * @see WidgetMapGenerator
+ *
+ */
+public class LazyWidgetMapGenerator extends WidgetMapGenerator {
+ @Override
+ protected LoadStyle getLoadStyle(Class<? extends ServerConnector> connector) {
+ return LoadStyle.LAZY;
+ }
+
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyCreatorGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyCreatorGenerator.java
new file mode 100644
index 0000000000..8a6c374187
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyCreatorGenerator.java
@@ -0,0 +1,126 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.util.Date;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.communication.InitializableServerRpc;
+import com.vaadin.terminal.gwt.client.communication.RpcProxy.RpcProxyCreator;
+
+public class RpcProxyCreatorGenerator extends Generator {
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext ctx,
+ String requestedClassName) throws UnableToCompleteException {
+ logger.log(TreeLogger.DEBUG, "Running RpcProxyCreatorGenerator");
+ TypeOracle typeOracle = ctx.getTypeOracle();
+ assert (typeOracle != null);
+
+ JClassType requestedType = typeOracle.findType(requestedClassName);
+ if (requestedType == null) {
+ logger.log(TreeLogger.ERROR, "Unable to find metadata for type '"
+ + requestedClassName + "'", null);
+ throw new UnableToCompleteException();
+ }
+ String packageName = requestedType.getPackage().getName();
+ String className = requestedType.getSimpleSourceName() + "Impl";
+
+ createType(logger, ctx, packageName, className);
+ return packageName + "." + className;
+ }
+
+ private void createType(TreeLogger logger, GeneratorContext context,
+ String packageName, String className) {
+ ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
+ packageName, className);
+
+ PrintWriter printWriter = context.tryCreate(logger,
+ composer.getCreatedPackage(),
+ composer.getCreatedClassShortName());
+ if (printWriter == null) {
+ // print writer is null if source code has already been generated
+ return;
+ }
+ Date date = new Date();
+ TypeOracle typeOracle = context.getTypeOracle();
+
+ // init composer, set class properties, create source writer
+ composer.addImport(GWT.class.getCanonicalName());
+ composer.addImport(ServerRpc.class.getCanonicalName());
+ composer.addImport(ServerConnector.class.getCanonicalName());
+ composer.addImport(InitializableServerRpc.class.getCanonicalName());
+ composer.addImport(IllegalArgumentException.class.getCanonicalName());
+ composer.addImplementedInterface(RpcProxyCreator.class
+ .getCanonicalName());
+
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+ sourceWriter.indent();
+
+ sourceWriter
+ .println("public <T extends ServerRpc> T create(Class<T> rpcInterface, ServerConnector connector) {");
+ sourceWriter.indent();
+
+ sourceWriter
+ .println("if (rpcInterface == null || connector == null) {");
+ sourceWriter.indent();
+ sourceWriter
+ .println("throw new IllegalArgumentException(\"RpcInterface and/or connector cannot be null\");");
+ sourceWriter.outdent();
+
+ JClassType initializableInterface = typeOracle.findType(ServerRpc.class
+ .getCanonicalName());
+
+ for (JClassType rpcType : initializableInterface.getSubtypes()) {
+ String rpcClassName = rpcType.getQualifiedSourceName();
+ if (InitializableServerRpc.class.getCanonicalName().equals(
+ rpcClassName)) {
+ // InitializableClientToServerRpc is a special marker interface
+ // that should not get a generated class
+ continue;
+ }
+ sourceWriter.println("} else if (rpcInterface == " + rpcClassName
+ + ".class) {");
+ sourceWriter.indent();
+ sourceWriter.println(rpcClassName + " rpc = GWT.create("
+ + rpcClassName + ".class);");
+ sourceWriter.println("((" + InitializableServerRpc.class.getName()
+ + ") rpc).initRpc(connector);");
+ sourceWriter.println("return (T) rpc;");
+ sourceWriter.outdent();
+ }
+
+ sourceWriter.println("} else {");
+ sourceWriter.indent();
+ sourceWriter
+ .println("throw new IllegalArgumentException(\"No RpcInterface of type \"+ rpcInterface.getName() + \" was found.\");");
+ sourceWriter.outdent();
+ // End of if
+ sourceWriter.println("}");
+ // End of method
+ sourceWriter.println("}");
+
+ // close generated class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(Type.INFO, composer.getCreatedClassName() + " created in "
+ + (new Date().getTime() - date.getTime()) / 1000 + "seconds");
+
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyGenerator.java
new file mode 100644
index 0000000000..7a908e5b4d
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/RpcProxyGenerator.java
@@ -0,0 +1,142 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.communication.InitializableServerRpc;
+
+/**
+ * GWT generator that creates client side proxy classes for making RPC calls
+ * from the client to the server.
+ *
+ * GWT.create() calls for interfaces extending {@link ServerRpc} are affected,
+ * and a proxy implementation is created. Note that the init(...) method of the
+ * proxy must be called before the proxy is used.
+ *
+ * @since 7.0
+ */
+public class RpcProxyGenerator extends Generator {
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext ctx,
+ String requestedClassName) throws UnableToCompleteException {
+ logger.log(TreeLogger.DEBUG, "Running RpcProxyGenerator", null);
+
+ TypeOracle typeOracle = ctx.getTypeOracle();
+ assert (typeOracle != null);
+
+ JClassType requestedType = typeOracle.findType(requestedClassName);
+ if (requestedType == null) {
+ logger.log(TreeLogger.ERROR, "Unable to find metadata for type '"
+ + requestedClassName + "'", null);
+ throw new UnableToCompleteException();
+ }
+
+ String generatedClassName = "ServerRpc_"
+ + requestedType.getName().replaceAll("[$.]", "_");
+
+ JClassType initializableInterface = typeOracle
+ .findType(InitializableServerRpc.class.getCanonicalName());
+
+ ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
+ requestedType.getPackage().getName(), generatedClassName);
+ composer.addImplementedInterface(requestedType.getQualifiedSourceName());
+ composer.addImplementedInterface(initializableInterface
+ .getQualifiedSourceName());
+ composer.addImport(MethodInvocation.class.getCanonicalName());
+
+ PrintWriter printWriter = ctx.tryCreate(logger,
+ composer.getCreatedPackage(),
+ composer.getCreatedClassShortName());
+ if (printWriter != null) {
+ logger.log(Type.INFO, "Generating client proxy for RPC interface '"
+ + requestedType.getQualifiedSourceName() + "'");
+ SourceWriter writer = composer.createSourceWriter(ctx, printWriter);
+
+ // constructor
+ writer.println("public " + generatedClassName + "() {}");
+
+ // initialization etc.
+ writeCommonFieldsAndMethods(logger, writer, typeOracle);
+
+ // actual proxy methods forwarding calls to the server
+ writeRemoteProxyMethods(logger, writer, typeOracle, requestedType,
+ requestedType.isClassOrInterface().getInheritableMethods());
+
+ // End of class
+ writer.outdent();
+ writer.println("}");
+
+ ctx.commit(logger, printWriter);
+ }
+
+ return composer.getCreatedClassName();
+ }
+
+ private void writeCommonFieldsAndMethods(TreeLogger logger,
+ SourceWriter writer, TypeOracle typeOracle) {
+ JClassType applicationConnectionClass = typeOracle
+ .findType(ApplicationConnection.class.getCanonicalName());
+
+ // fields
+ writer.println("private " + ServerConnector.class.getName()
+ + " connector;");
+
+ // init method from the RPC interface
+ writer.println("public void initRpc(" + ServerConnector.class.getName()
+ + " connector) {");
+ writer.indent();
+ writer.println("this.connector = connector;");
+ writer.outdent();
+ writer.println("}");
+ }
+
+ private static void writeRemoteProxyMethods(TreeLogger logger,
+ SourceWriter writer, TypeOracle typeOracle,
+ JClassType requestedType, JMethod[] methods) {
+ for (JMethod m : methods) {
+ writer.print(m.getReadableDeclaration(false, false, false, false,
+ true));
+ writer.println(" {");
+ writer.indent();
+
+ writer.print("this.connector.getConnection().addMethodInvocationToQueue(new MethodInvocation(this.connector.getConnectorId(), \""
+ + requestedType.getQualifiedBinaryName() + "\", \"");
+ writer.print(m.getName());
+ writer.print("\", new Object[] {");
+ // new Object[] { ... } for parameters - autoboxing etc. by the
+ // compiler
+ JParameter[] parameters = m.getParameters();
+ boolean first = true;
+ for (JParameter p : parameters) {
+ if (!first) {
+ writer.print(", ");
+ }
+ first = false;
+
+ writer.print(p.getName());
+ }
+ writer.println("}), true);");
+
+ writer.outdent();
+ writer.println("}");
+ }
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerGenerator.java
new file mode 100644
index 0000000000..1951f8ba40
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerGenerator.java
@@ -0,0 +1,458 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JArrayType;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JEnumConstant;
+import com.google.gwt.core.ext.typeinfo.JEnumType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.TypeOracleException;
+import com.google.gwt.json.client.JSONArray;
+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.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.communication.DiffJSONSerializer;
+import com.vaadin.terminal.gwt.client.communication.JSONSerializer;
+import com.vaadin.terminal.gwt.client.communication.JsonDecoder;
+import com.vaadin.terminal.gwt.client.communication.JsonEncoder;
+import com.vaadin.terminal.gwt.client.communication.SerializerMap;
+
+/**
+ * GWT generator for creating serializer classes for custom classes sent from
+ * server to client.
+ *
+ * Only fields with a correspondingly named setter are deserialized.
+ *
+ * @since 7.0
+ */
+public class SerializerGenerator extends Generator {
+
+ private static final String SUBTYPE_SEPARATOR = "___";
+ private static String serializerPackageName = SerializerMap.class
+ .getPackage().getName();
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext context,
+ String typeName) throws UnableToCompleteException {
+ JClassType type;
+ try {
+ type = (JClassType) context.getTypeOracle().parse(typeName);
+ } catch (TypeOracleException e1) {
+ logger.log(Type.ERROR, "Could not find type " + typeName, e1);
+ throw new UnableToCompleteException();
+ }
+ String serializerClassName = getSerializerSimpleClassName(type);
+ try {
+ // Generate class source code
+ generateClass(logger, context, type, serializerPackageName,
+ serializerClassName);
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR, "SerializerGenerator failed for "
+ + type.getQualifiedSourceName(), e);
+ throw new UnableToCompleteException();
+ }
+
+ // return the fully qualifed name of the class generated
+ return getFullyQualifiedSerializerClassName(type);
+ }
+
+ /**
+ * Generate source code for a VaadinSerializer implementation.
+ *
+ * @param logger
+ * Logger object
+ * @param context
+ * Generator context
+ * @param type
+ * @param beanTypeName
+ * bean type for which the serializer is to be generated
+ * @param beanSerializerTypeName
+ * name of the serializer class to generate
+ * @throws UnableToCompleteException
+ */
+ private void generateClass(TreeLogger logger, GeneratorContext context,
+ JClassType type, String serializerPackageName,
+ String serializerClassName) throws UnableToCompleteException {
+ // get print writer that receives the source code
+ PrintWriter printWriter = null;
+ printWriter = context.tryCreate(logger, serializerPackageName,
+ serializerClassName);
+
+ // print writer if null, source code has ALREADY been generated
+ if (printWriter == null) {
+ return;
+ }
+ boolean isEnum = (type.isEnum() != null);
+ boolean isArray = (type.isArray() != null);
+
+ String qualifiedSourceName = type.getQualifiedSourceName();
+ logger.log(Type.DEBUG, "Processing serializable type "
+ + qualifiedSourceName + "...");
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(serializerPackageName,
+ serializerClassName);
+ composer.addImport(GWT.class.getName());
+ composer.addImport(JSONValue.class.getName());
+ composer.addImport(com.vaadin.terminal.gwt.client.communication.Type.class
+ .getName());
+ // composer.addImport(JSONObject.class.getName());
+ // composer.addImport(VPaintableMap.class.getName());
+ composer.addImport(JsonDecoder.class.getName());
+ // composer.addImport(VaadinSerializer.class.getName());
+
+ if (isEnum || isArray) {
+ composer.addImplementedInterface(JSONSerializer.class.getName()
+ + "<" + qualifiedSourceName + ">");
+ } else {
+ composer.addImplementedInterface(DiffJSONSerializer.class.getName()
+ + "<" + qualifiedSourceName + ">");
+ }
+
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+ sourceWriter.indent();
+
+ // Serializer
+
+ // public JSONValue serialize(Object value,
+ // ApplicationConnection connection) {
+ sourceWriter.println("public " + JSONValue.class.getName()
+ + " serialize(" + qualifiedSourceName + " value, "
+ + ApplicationConnection.class.getName() + " connection) {");
+ sourceWriter.indent();
+ // MouseEventDetails castedValue = (MouseEventDetails) value;
+ sourceWriter.println(qualifiedSourceName + " castedValue = ("
+ + qualifiedSourceName + ") value;");
+
+ if (isEnum) {
+ writeEnumSerializer(logger, sourceWriter, type);
+ } else if (isArray) {
+ writeArraySerializer(logger, sourceWriter, type.isArray());
+ } else {
+ writeBeanSerializer(logger, sourceWriter, type);
+ }
+ // }
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ sourceWriter.println();
+
+ // Updater
+ // public void update(T target, Type type, JSONValue jsonValue,
+ // ApplicationConnection connection);
+ if (!isEnum && !isArray) {
+ sourceWriter.println("public void update(" + qualifiedSourceName
+ + " target, Type type, " + JSONValue.class.getName()
+ + " jsonValue, " + ApplicationConnection.class.getName()
+ + " connection) {");
+ sourceWriter.indent();
+
+ writeBeanDeserializer(logger, sourceWriter, type);
+
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ }
+
+ // Deserializer
+ // T deserialize(Type type, JSONValue jsonValue, ApplicationConnection
+ // connection);
+ sourceWriter.println("public " + qualifiedSourceName
+ + " deserialize(Type type, " + JSONValue.class.getName()
+ + " jsonValue, " + ApplicationConnection.class.getName()
+ + " connection) {");
+ sourceWriter.indent();
+
+ if (isEnum) {
+ writeEnumDeserializer(logger, sourceWriter, type.isEnum());
+ } else if (isArray) {
+ writeArrayDeserializer(logger, sourceWriter, type.isArray());
+ } else {
+ sourceWriter.println(qualifiedSourceName + " target = GWT.create("
+ + qualifiedSourceName + ".class);");
+ sourceWriter
+ .println("update(target, type, jsonValue, connection);");
+ // return target;
+ sourceWriter.println("return target;");
+ }
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ // End of class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(TreeLogger.INFO, "Generated Serializer class "
+ + getFullyQualifiedSerializerClassName(type));
+ }
+
+ private void writeEnumDeserializer(TreeLogger logger,
+ SourceWriter sourceWriter, JEnumType enumType) {
+ sourceWriter.println("String enumIdentifier = (("
+ + JSONString.class.getName() + ")jsonValue).stringValue();");
+ for (JEnumConstant e : enumType.getEnumConstants()) {
+ sourceWriter.println("if (\"" + e.getName()
+ + "\".equals(enumIdentifier)) {");
+ sourceWriter.indent();
+ sourceWriter.println("return " + enumType.getQualifiedSourceName()
+ + "." + e.getName() + ";");
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ }
+ sourceWriter.println("return null;");
+ }
+
+ private void writeArrayDeserializer(TreeLogger logger,
+ SourceWriter sourceWriter, JArrayType type) {
+ JType leafType = type.getLeafType();
+ int rank = type.getRank();
+
+ sourceWriter.println(JSONArray.class.getName()
+ + " jsonArray = jsonValue.isArray();");
+
+ // Type value = new Type[jsonArray.size()][][];
+ sourceWriter.print(type.getQualifiedSourceName() + " value = new "
+ + leafType.getQualifiedSourceName() + "[jsonArray.size()]");
+ for (int i = 1; i < rank; i++) {
+ sourceWriter.print("[]");
+ }
+ sourceWriter.println(";");
+
+ sourceWriter.println("for(int i = 0 ; i < value.length; i++) {");
+ sourceWriter.indent();
+
+ JType componentType = type.getComponentType();
+
+ sourceWriter.print("value[i] = ("
+ + GeneratedRpcMethodProviderGenerator
+ .getBoxedTypeName(componentType) + ") "
+ + JsonDecoder.class.getName() + ".decodeValue(");
+ GeneratedRpcMethodProviderGenerator.writeTypeCreator(sourceWriter,
+ componentType);
+ sourceWriter.print(", jsonArray.get(i), null, connection)");
+
+ sourceWriter.println(";");
+
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ sourceWriter.println("return value;");
+ }
+
+ private void writeBeanDeserializer(TreeLogger logger,
+ SourceWriter sourceWriter, JClassType beanType) {
+ String beanQualifiedSourceName = beanType.getQualifiedSourceName();
+
+ // JSONOBject json = (JSONObject)jsonValue;
+ sourceWriter.println(JSONObject.class.getName() + " json = ("
+ + JSONObject.class.getName() + ")jsonValue;");
+
+ for (JMethod method : getSetters(beanType)) {
+ String setterName = method.getName();
+ String baseName = setterName.substring(3);
+ String fieldName = getTransportFieldName(baseName); // setZIndex()
+ // -> zIndex
+ JType setterParameterType = method.getParameterTypes()[0];
+
+ logger.log(Type.DEBUG, "* Processing field " + fieldName + " in "
+ + beanQualifiedSourceName + " (" + beanType.getName() + ")");
+
+ // if (json.containsKey("height")) {
+ sourceWriter.println("if (json.containsKey(\"" + fieldName
+ + "\")) {");
+ sourceWriter.indent();
+ String jsonFieldName = "json_" + fieldName;
+ // JSONValue json_Height = json.get("height");
+ sourceWriter.println("JSONValue " + jsonFieldName
+ + " = json.get(\"" + fieldName + "\");");
+
+ String fieldType;
+ String getterName = "get" + baseName;
+ JPrimitiveType primitiveType = setterParameterType.isPrimitive();
+ if (primitiveType != null) {
+ // This is a primitive type -> must used the boxed type
+ fieldType = primitiveType.getQualifiedBoxedSourceName();
+ if (primitiveType == JPrimitiveType.BOOLEAN) {
+ getterName = "is" + baseName;
+ }
+ } else {
+ fieldType = setterParameterType.getQualifiedSourceName();
+ }
+
+ // String referenceValue = target.getHeight();
+ sourceWriter.println(fieldType + " referenceValue = target."
+ + getterName + "();");
+
+ // target.setHeight((String)
+ // JsonDecoder.decodeValue(jsonFieldValue,referenceValue, idMapper,
+ // connection));
+ sourceWriter.print("target." + setterName + "((" + fieldType + ") "
+ + JsonDecoder.class.getName() + ".decodeValue(");
+ GeneratedRpcMethodProviderGenerator.writeTypeCreator(sourceWriter,
+ setterParameterType);
+ sourceWriter.println(", " + jsonFieldName
+ + ", referenceValue, connection));");
+
+ // } ... end of if contains
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ }
+ }
+
+ private void writeEnumSerializer(TreeLogger logger,
+ SourceWriter sourceWriter, JClassType beanType) {
+ // return new JSONString(castedValue.name());
+ sourceWriter.println("return new " + JSONString.class.getName()
+ + "(castedValue.name());");
+ }
+
+ private void writeArraySerializer(TreeLogger logger,
+ SourceWriter sourceWriter, JArrayType array) {
+ sourceWriter.println(JSONArray.class.getName() + " values = new "
+ + JSONArray.class.getName() + "();");
+ JType componentType = array.getComponentType();
+ // JPrimitiveType primitive = componentType.isPrimitive();
+ sourceWriter.println("for (int i = 0; i < castedValue.length; i++) {");
+ sourceWriter.indent();
+ sourceWriter.print("values.set(i, ");
+ sourceWriter.print(JsonEncoder.class.getName()
+ + ".encode(castedValue[i], false, connection)");
+ sourceWriter.println(");");
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ sourceWriter.println("return values;");
+ }
+
+ private void writeBeanSerializer(TreeLogger logger,
+ SourceWriter sourceWriter, JClassType beanType)
+ throws UnableToCompleteException {
+
+ // JSONObject json = new JSONObject();
+ sourceWriter.println(JSONObject.class.getName() + " json = new "
+ + JSONObject.class.getName() + "();");
+
+ HashSet<String> usedFieldNames = new HashSet<String>();
+
+ for (JMethod setterMethod : getSetters(beanType)) {
+ String setterName = setterMethod.getName();
+ String fieldName = getTransportFieldName(setterName.substring(3)); // setZIndex()
+ // -> zIndex
+ if (!usedFieldNames.add(fieldName)) {
+ logger.log(
+ TreeLogger.ERROR,
+ "Can't encode "
+ + beanType.getQualifiedSourceName()
+ + " as it has multiple fields with the name "
+ + fieldName.toLowerCase()
+ + ". This can happen if only casing distinguishes one property name from another.");
+ throw new UnableToCompleteException();
+ }
+ String getterName = findGetter(beanType, setterMethod);
+
+ if (getterName == null) {
+ logger.log(TreeLogger.ERROR, "No getter found for " + fieldName
+ + ". Serialization will likely fail");
+ }
+ // json.put("button",
+ // JsonEncoder.encode(castedValue.getButton(), false, idMapper,
+ // connection));
+ sourceWriter.println("json.put(\"" + fieldName + "\", "
+ + JsonEncoder.class.getName() + ".encode(castedValue."
+ + getterName + "(), false, connection));");
+ }
+ // return json;
+ sourceWriter.println("return json;");
+
+ }
+
+ private static String getTransportFieldName(String baseName) {
+ return Character.toLowerCase(baseName.charAt(0))
+ + baseName.substring(1);
+ }
+
+ private String findGetter(JClassType beanType, JMethod setterMethod) {
+ JType setterParameterType = setterMethod.getParameterTypes()[0];
+ String fieldName = setterMethod.getName().substring(3);
+ if (setterParameterType.getQualifiedSourceName().equals(
+ boolean.class.getName())) {
+ return "is" + fieldName;
+ } else {
+ return "get" + fieldName;
+ }
+ }
+
+ /**
+ * Returns a list of all setters found in the beanType or its parent class
+ *
+ * @param beanType
+ * The type to check
+ * @return A list of setter methods from the class and its parents
+ */
+ protected static List<JMethod> getSetters(JClassType beanType) {
+
+ List<JMethod> setterMethods = new ArrayList<JMethod>();
+
+ while (beanType != null
+ && !beanType.getQualifiedSourceName().equals(
+ Object.class.getName())) {
+ for (JMethod method : beanType.getMethods()) {
+ // Process all setters that have corresponding fields
+ if (!method.isPublic() || method.isStatic()
+ || !method.getName().startsWith("set")
+ || method.getParameterTypes().length != 1) {
+ // Not setter, skip to next method
+ continue;
+ }
+ setterMethods.add(method);
+ }
+ beanType = beanType.getSuperclass();
+ }
+
+ return setterMethods;
+ }
+
+ private static String getSerializerSimpleClassName(JClassType beanType) {
+ return getSimpleClassName(beanType) + "_Serializer";
+ }
+
+ private static String getSimpleClassName(JType type) {
+ JArrayType arrayType = type.isArray();
+ if (arrayType != null) {
+ return "Array" + getSimpleClassName(arrayType.getComponentType());
+ }
+ JClassType classType = type.isClass();
+ if (classType != null && classType.isMemberType()) {
+ // Assumed to be static sub class
+ String baseName = getSimpleClassName(classType.getEnclosingType());
+ String name = baseName + SUBTYPE_SEPARATOR
+ + type.getSimpleSourceName();
+ return name;
+ }
+ return type.getSimpleSourceName();
+ }
+
+ public static String getFullyQualifiedSerializerClassName(JClassType type) {
+ return serializerPackageName + "." + getSerializerSimpleClassName(type);
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java
new file mode 100644
index 0000000000..3f1ad24066
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java
@@ -0,0 +1,365 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JArrayType;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.communication.JSONSerializer;
+import com.vaadin.terminal.gwt.client.communication.SerializerMap;
+
+/**
+ * GWT generator that creates a {@link SerializerMap} implementation (mapper
+ * from type string to serializer instance) and serializer classes for all
+ * subclasses of {@link SharedState}.
+ *
+ * @since 7.0
+ */
+public class SerializerMapGenerator extends Generator {
+
+ private static final String FAIL_IF_NOT_SERIALIZABLE = "vFailIfNotSerializable";
+ private String packageName;
+ private String className;
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext context,
+ String typeName) throws UnableToCompleteException {
+
+ try {
+ TypeOracle typeOracle = context.getTypeOracle();
+ Set<JClassType> typesNeedingSerializers = findTypesNeedingSerializers(
+ typeOracle, logger);
+ checkForUnserializableTypes(typesNeedingSerializers, typeOracle,
+ logger);
+ Set<JClassType> typesWithExistingSerializers = findTypesWithExistingSerializers(
+ typeOracle, logger);
+ Set<JClassType> serializerMappings = new HashSet<JClassType>();
+ serializerMappings.addAll(typesNeedingSerializers);
+ serializerMappings.addAll(typesWithExistingSerializers);
+ // get classType and save instance variables
+ JClassType classType = typeOracle.getType(typeName);
+ packageName = classType.getPackage().getName();
+ className = classType.getSimpleSourceName() + "Impl";
+ // Generate class source code for SerializerMapImpl
+ generateSerializerMap(serializerMappings, logger, context);
+
+ SerializerGenerator sg = new SerializerGenerator();
+ for (JClassType type : typesNeedingSerializers) {
+ sg.generate(logger, context, type.getQualifiedSourceName());
+ }
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR,
+ "SerializerMapGenerator creation failed", e);
+ throw new UnableToCompleteException();
+ }
+ // return the fully qualifed name of the class generated
+ return packageName + "." + className;
+ }
+
+ /**
+ * Emits a warning for all classes that are used in communication but do not
+ * implement java.io.Serializable. Implementing java.io.Serializable is not
+ * needed for communication but for the server side Application to be
+ * serializable i.e. work in GAE for instance.
+ *
+ * @param typesNeedingSerializers
+ * @param typeOracle
+ * @param logger
+ * @throws UnableToCompleteException
+ */
+ private void checkForUnserializableTypes(
+ Set<JClassType> typesNeedingSerializers, TypeOracle typeOracle,
+ TreeLogger logger) throws UnableToCompleteException {
+ JClassType javaSerializable = typeOracle.findType(Serializable.class
+ .getName());
+ for (JClassType type : typesNeedingSerializers) {
+ if (type.isArray() != null) {
+ // Don't check for arrays
+ continue;
+ }
+ boolean serializable = type.isAssignableTo(javaSerializable);
+ if (!serializable) {
+ boolean abortCompile = "true".equals(System
+ .getProperty(FAIL_IF_NOT_SERIALIZABLE));
+ logger.log(
+ abortCompile ? Type.ERROR : Type.WARN,
+ type
+ + " is used in RPC or shared state but does not implement "
+ + Serializable.class.getName()
+ + ". Communication will work but the Application on server side cannot be serialized if it refers to objects of this type. "
+ + "If the system property "
+ + FAIL_IF_NOT_SERIALIZABLE
+ + " is set to \"true\", this causes the compilation to fail instead of just emitting a warning.");
+ if (abortCompile) {
+ throw new UnableToCompleteException();
+ }
+ }
+ }
+ }
+
+ private Set<JClassType> findTypesWithExistingSerializers(
+ TypeOracle typeOracle, TreeLogger logger)
+ throws UnableToCompleteException {
+ JClassType serializerInterface = typeOracle
+ .findType(JSONSerializer.class.getName());
+ JType[] deserializeParamTypes = new JType[] {
+ typeOracle
+ .findType(com.vaadin.terminal.gwt.client.communication.Type.class
+ .getName()),
+ typeOracle.findType(JSONValue.class.getName()),
+ typeOracle.findType(ApplicationConnection.class.getName()) };
+ String deserializeMethodName = "deserialize";
+ try {
+ serializerInterface.getMethod(deserializeMethodName,
+ deserializeParamTypes);
+ } catch (NotFoundException e) {
+ logger.log(Type.ERROR, "Could not find " + deserializeMethodName
+ + " in " + serializerInterface);
+ throw new UnableToCompleteException();
+ }
+
+ Set<JClassType> types = new HashSet<JClassType>();
+ for (JClassType serializer : serializerInterface.getSubtypes()) {
+ JMethod deserializeMethod = serializer.findMethod(
+ deserializeMethodName, deserializeParamTypes);
+ if (deserializeMethod == null) {
+ logger.log(Type.DEBUG, "Could not find "
+ + deserializeMethodName + " in " + serializer);
+ continue;
+ }
+ JType returnType = deserializeMethod.getReturnType();
+ logger.log(Type.DEBUG, "Found " + deserializeMethodName
+ + " with return type " + returnType + " in " + serializer);
+
+ types.add(returnType.isClass());
+ }
+ return types;
+ }
+
+ /**
+ * Generate source code for SerializerMapImpl
+ *
+ * @param typesNeedingSerializers
+ *
+ * @param logger
+ * Logger object
+ * @param context
+ * Generator context
+ */
+ private void generateSerializerMap(Set<JClassType> typesNeedingSerializers,
+ TreeLogger logger, GeneratorContext context) {
+ // get print writer that receives the source code
+ PrintWriter printWriter = null;
+ printWriter = context.tryCreate(logger, packageName, className);
+ // print writer if null, source code has ALREADY been generated
+ if (printWriter == null) {
+ return;
+ }
+ Date date = new Date();
+ TypeOracle typeOracle = context.getTypeOracle();
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(packageName, className);
+ composer.addImport("com.google.gwt.core.client.GWT");
+ composer.addImplementedInterface(SerializerMap.class.getName());
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+ sourceWriter.indent();
+
+ sourceWriter.println("public " + JSONSerializer.class.getName()
+ + " getSerializer(String type) {");
+ sourceWriter.indent();
+
+ // TODO cache serializer instances in a map
+ for (JClassType type : typesNeedingSerializers) {
+ sourceWriter.print("if (type.equals(\""
+ + type.getQualifiedSourceName() + "\")");
+ if (type instanceof JArrayType) {
+ // Also add binary name to support encoding based on
+ // object.getClass().getName()
+ sourceWriter.print("||type.equals(\"" + type.getJNISignature()
+ + "\")");
+ }
+ sourceWriter.println(") {");
+ sourceWriter.indent();
+ String serializerName = SerializerGenerator
+ .getFullyQualifiedSerializerClassName(type);
+ sourceWriter.println("return GWT.create(" + serializerName
+ + ".class);");
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ logger.log(Type.INFO, "Configured serializer (" + serializerName
+ + ") for " + type.getName());
+ }
+ sourceWriter
+ .println("throw new RuntimeException(\"No serializer found for class \"+type);");
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ // close generated class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(Type.INFO,
+ "Done. (" + (new Date().getTime() - date.getTime()) / 1000
+ + "seconds)");
+
+ }
+
+ public Set<JClassType> findTypesNeedingSerializers(TypeOracle typeOracle,
+ TreeLogger logger) {
+ logger.log(Type.DEBUG, "Detecting serializable data types...");
+
+ HashSet<JClassType> types = new HashSet<JClassType>();
+
+ // Generate serializer classes for each subclass of SharedState
+ JClassType serializerType = typeOracle.findType(SharedState.class
+ .getName());
+ types.add(serializerType);
+ JClassType[] serializerSubtypes = serializerType.getSubtypes();
+ for (JClassType type : serializerSubtypes) {
+ types.add(type);
+ }
+
+ // Serializer classes might also be needed for RPC methods
+ for (Class<?> cls : new Class[] { ServerRpc.class, ClientRpc.class }) {
+ JClassType rpcType = typeOracle.findType(cls.getName());
+ JClassType[] serverRpcSubtypes = rpcType.getSubtypes();
+ for (JClassType type : serverRpcSubtypes) {
+ addMethodParameterTypes(type, types, logger);
+ }
+ }
+
+ // Add all types used from/in the types
+ for (Object t : types.toArray()) {
+ findSubTypesNeedingSerializers((JClassType) t, types);
+ }
+ logger.log(Type.DEBUG, "Serializable data types: " + types.toString());
+
+ return types;
+ }
+
+ private void addMethodParameterTypes(JClassType classContainingMethods,
+ Set<JClassType> types, TreeLogger logger) {
+ for (JMethod method : classContainingMethods.getMethods()) {
+ if (method.getName().equals("initRpc")) {
+ continue;
+ }
+ for (JType type : method.getParameterTypes()) {
+ addTypeIfNeeded(types, type);
+ }
+ }
+ }
+
+ public void findSubTypesNeedingSerializers(JClassType type,
+ Set<JClassType> serializableTypes) {
+ // Find all setters and look at their parameter type to determine if a
+ // new serializer is needed
+ for (JMethod setterMethod : SerializerGenerator.getSetters(type)) {
+ // The one and only parameter for the setter
+ JType setterType = setterMethod.getParameterTypes()[0];
+ addTypeIfNeeded(serializableTypes, setterType);
+ }
+ }
+
+ private void addTypeIfNeeded(Set<JClassType> serializableTypes, JType type) {
+ if (serializableTypes.contains(type)) {
+ return;
+ }
+ JParameterizedType parametrized = type.isParameterized();
+ if (parametrized != null) {
+ for (JClassType parameterType : parametrized.getTypeArgs()) {
+ addTypeIfNeeded(serializableTypes, parameterType);
+ }
+ }
+
+ if (serializationHandledByFramework(type)) {
+ return;
+ }
+
+ if (serializableTypes.contains(type)) {
+ return;
+ }
+
+ JClassType typeClass = type.isClass();
+ if (typeClass != null) {
+ // setterTypeClass is null at least for List<String>. It is
+ // possible that we need to handle the cases somehow, for
+ // instance for List<MyObject>.
+ serializableTypes.add(typeClass);
+ findSubTypesNeedingSerializers(typeClass, serializableTypes);
+ }
+
+ // Generate (n-1)-dimensional array serializer for n-dimensional array
+ JArrayType arrayType = type.isArray();
+ if (arrayType != null) {
+ serializableTypes.add(arrayType);
+ addTypeIfNeeded(serializableTypes, arrayType.getComponentType());
+ }
+
+ }
+
+ Set<Class<?>> frameworkHandledTypes = new HashSet<Class<?>>();
+ {
+ frameworkHandledTypes.add(String.class);
+ frameworkHandledTypes.add(Boolean.class);
+ frameworkHandledTypes.add(Integer.class);
+ frameworkHandledTypes.add(Float.class);
+ frameworkHandledTypes.add(Double.class);
+ frameworkHandledTypes.add(Long.class);
+ frameworkHandledTypes.add(Enum.class);
+ frameworkHandledTypes.add(String[].class);
+ frameworkHandledTypes.add(Object[].class);
+ frameworkHandledTypes.add(Map.class);
+ frameworkHandledTypes.add(List.class);
+ frameworkHandledTypes.add(Set.class);
+ frameworkHandledTypes.add(Byte.class);
+ frameworkHandledTypes.add(Character.class);
+
+ }
+
+ private boolean serializationHandledByFramework(JType setterType) {
+ // Some types are handled by the framework at the moment. See #8449
+ // This method should be removed at some point.
+ if (setterType.isPrimitive() != null) {
+ return true;
+ }
+
+ String qualifiedName = setterType.getQualifiedSourceName();
+ for (Class<?> cls : frameworkHandledTypes) {
+ if (qualifiedName.equals(cls.getName())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetMapGenerator.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetMapGenerator.java
new file mode 100644
index 0000000000..0d062ec4ff
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetMapGenerator.java
@@ -0,0 +1,398 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.TreeSet;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.client.ui.UnknownComponentConnector;
+import com.vaadin.terminal.gwt.client.ui.root.RootConnector;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+
+/**
+ * WidgetMapGenerator's are GWT generator to build WidgetMapImpl dynamically
+ * based on {@link Connect} annotations available in workspace. By modifying the
+ * generator it is possible to do some fine tuning for the generated widgetset
+ * (aka client side engine). The components to be included in the client side
+ * engine can modified be overriding {@link #getUsedConnectors()}.
+ * <p>
+ * The generator also decides how the client side component implementations are
+ * loaded to the browser. The default generator is
+ * {@link EagerWidgetMapGenerator} that builds a monolithic client side engine
+ * that loads all widget implementation on application initialization. This has
+ * been the only option until Vaadin 6.4.
+ * <p>
+ * This generator uses the loadStyle hints from the {@link Connect} annotations.
+ * Depending on the {@link LoadStyle} used, the widget may be included in the
+ * initially loaded JavaScript, loaded when the application has started and
+ * there is no communication to server or lazy loaded when the implementation is
+ * absolutely needed.
+ * <p>
+ * The GWT module description file of the widgetset (
+ * <code>...Widgetset.gwt.xml</code>) can be used to define the
+ * WidgetMapGenarator. An example that defines this generator to be used:
+ *
+ * <pre>
+ * <code>
+ * &lt;generate-with
+ * class="com.vaadin.terminal.gwt.widgetsetutils.MyWidgetMapGenerator"&gt;
+ * &lt;when-type-is class="com.vaadin.terminal.gwt.client.WidgetMap" /&gt;
+ * &lt;/generate-with&gt;
+ *
+ * </code>
+ * </pre>
+ *
+ * <p>
+ * Vaadin package also includes {@link LazyWidgetMapGenerator}, which is a good
+ * option if the transferred data should be minimized, and
+ * {@link CustomWidgetMapGenerator} for easy overriding of loading strategies.
+ *
+ */
+public class WidgetMapGenerator extends Generator {
+
+ private static String serverConnectorClassName = ServerConnector.class
+ .getName();
+
+ private String packageName;
+ private String className;
+
+ @Override
+ public String generate(TreeLogger logger, GeneratorContext context,
+ String typeName) throws UnableToCompleteException {
+
+ try {
+ TypeOracle typeOracle = context.getTypeOracle();
+
+ // get classType and save instance variables
+ JClassType classType = typeOracle.getType(typeName);
+ packageName = classType.getPackage().getName();
+ className = classType.getSimpleSourceName() + "Impl";
+ // Generate class source code
+ generateClass(logger, context);
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR, "WidgetMap creation failed", e);
+ }
+ // return the fully qualifed name of the class generated
+ return packageName + "." + className;
+ }
+
+ /**
+ * Generate source code for WidgetMapImpl
+ *
+ * @param logger
+ * Logger object
+ * @param context
+ * Generator context
+ */
+ private void generateClass(TreeLogger logger, GeneratorContext context) {
+ // get print writer that receives the source code
+ PrintWriter printWriter = null;
+ printWriter = context.tryCreate(logger, packageName, className);
+ // print writer if null, source code has ALREADY been generated,
+ // return (WidgetMap is equal to all permutations atm)
+ if (printWriter == null) {
+ return;
+ }
+ logger.log(Type.INFO,
+ "Detecting Vaadin connectors in classpath to generate WidgetMapImpl.java ...");
+ Date date = new Date();
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(packageName, className);
+ composer.addImport("com.google.gwt.core.client.GWT");
+ composer.addImport("java.util.HashMap");
+ composer.addImport("com.google.gwt.core.client.RunAsyncCallback");
+ composer.setSuperclass("com.vaadin.terminal.gwt.client.WidgetMap");
+ SourceWriter sourceWriter = composer.createSourceWriter(context,
+ printWriter);
+
+ Collection<Class<? extends ServerConnector>> connectors = getUsedConnectors(context
+ .getTypeOracle());
+
+ validateConnectors(logger, connectors);
+ logConnectors(logger, context, connectors);
+
+ // generator constructor source code
+ generateImplementationDetector(sourceWriter, connectors);
+ generateInstantiatorMethod(sourceWriter, connectors);
+ // close generated class
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+ // commit generated class
+ context.commit(logger, printWriter);
+ logger.log(Type.INFO,
+ "Done. (" + (new Date().getTime() - date.getTime()) / 1000
+ + "seconds)");
+
+ }
+
+ private void validateConnectors(TreeLogger logger,
+ Collection<Class<? extends ServerConnector>> connectors) {
+
+ Iterator<Class<? extends ServerConnector>> iter = connectors.iterator();
+ while (iter.hasNext()) {
+ Class<? extends ServerConnector> connectorClass = iter.next();
+ Connect annotation = connectorClass.getAnnotation(Connect.class);
+ if (!ClientConnector.class.isAssignableFrom(annotation.value())) {
+ logger.log(
+ Type.WARN,
+ "Connector class "
+ + annotation.value().getName()
+ + " defined in @Connect annotation is not a subclass of "
+ + ClientConnector.class.getName()
+ + ". The component connector "
+ + connectorClass.getName()
+ + " will not be included in the widgetset.");
+ iter.remove();
+ }
+ }
+
+ }
+
+ private void logConnectors(TreeLogger logger, GeneratorContext context,
+ Collection<Class<? extends ServerConnector>> connectors) {
+ logger.log(Type.INFO,
+ "Widget set will contain implementations for following component connectors: ");
+
+ TreeSet<String> classNames = new TreeSet<String>();
+ HashMap<String, String> loadStyle = new HashMap<String, String>();
+ for (Class<? extends ServerConnector> connectorClass : connectors) {
+ String className = connectorClass.getCanonicalName();
+ classNames.add(className);
+ if (getLoadStyle(connectorClass) == LoadStyle.DEFERRED) {
+ loadStyle.put(className, "DEFERRED");
+ } else if (getLoadStyle(connectorClass) == LoadStyle.LAZY) {
+ loadStyle.put(className, "LAZY");
+ }
+
+ }
+ for (String className : classNames) {
+ String msg = className;
+ if (loadStyle.containsKey(className)) {
+ msg += " (load style: " + loadStyle.get(className) + ")";
+ }
+ logger.log(Type.INFO, "\t" + msg);
+ }
+ }
+
+ /**
+ * This method is protected to allow creation of optimized widgetsets. The
+ * Widgetset will contain only implementation returned by this function. If
+ * one knows which widgets are needed for the application, returning only
+ * them here will significantly optimize the size of the produced JS.
+ *
+ * @return a collections of Vaadin components that will be added to
+ * widgetset
+ */
+ @SuppressWarnings("unchecked")
+ private Collection<Class<? extends ServerConnector>> getUsedConnectors(
+ TypeOracle typeOracle) {
+ JClassType connectorType = typeOracle.findType(Connector.class
+ .getName());
+ Collection<Class<? extends ServerConnector>> connectors = new HashSet<Class<? extends ServerConnector>>();
+ for (JClassType jClassType : connectorType.getSubtypes()) {
+ Connect annotation = jClassType.getAnnotation(Connect.class);
+ if (annotation != null) {
+ try {
+ Class<? extends ServerConnector> clazz = (Class<? extends ServerConnector>) Class
+ .forName(jClassType.getQualifiedSourceName());
+ connectors.add(clazz);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return connectors;
+ }
+
+ /**
+ * Returns true if the widget for given component will be lazy loaded by the
+ * client. The default implementation reads the information from the
+ * {@link Connect} annotation.
+ * <p>
+ * The method can be overridden to optimize the widget loading mechanism. If
+ * the Widgetset is wanted to be optimized for a network with a high latency
+ * or for a one with a very fast throughput, it may be good to return false
+ * for every component.
+ *
+ * @param connector
+ * @return true iff the widget for given component should be lazy loaded by
+ * the client side engine
+ */
+ protected LoadStyle getLoadStyle(Class<? extends ServerConnector> connector) {
+ Connect annotation = connector.getAnnotation(Connect.class);
+ return annotation.loadStyle();
+ }
+
+ private void generateInstantiatorMethod(
+ SourceWriter sourceWriter,
+ Collection<Class<? extends ServerConnector>> connectorsHavingComponentAnnotation) {
+
+ Collection<Class<?>> deferredWidgets = new LinkedList<Class<?>>();
+
+ // TODO detect if it would be noticably faster to instantiate with a
+ // lookup with index than with the hashmap
+
+ sourceWriter.println("public void ensureInstantiator(Class<? extends "
+ + serverConnectorClassName + "> classType) {");
+ sourceWriter.println("if(!instmap.containsKey(classType)){");
+ boolean first = true;
+
+ ArrayList<Class<? extends ServerConnector>> lazyLoadedConnectors = new ArrayList<Class<? extends ServerConnector>>();
+
+ HashSet<Class<? extends ServerConnector>> connectorsWithInstantiator = new HashSet<Class<? extends ServerConnector>>();
+
+ for (Class<? extends ServerConnector> class1 : connectorsHavingComponentAnnotation) {
+ Class<? extends ServerConnector> clientClass = class1;
+ if (connectorsWithInstantiator.contains(clientClass)) {
+ continue;
+ }
+ if (clientClass == RootConnector.class) {
+ // Roots are not instantiated by widgetset
+ continue;
+ }
+ if (!first) {
+ sourceWriter.print(" else ");
+ } else {
+ first = false;
+ }
+ sourceWriter.print("if( classType == " + clientClass.getName()
+ + ".class) {");
+
+ String instantiator = "new WidgetInstantiator() {\n public "
+ + serverConnectorClassName
+ + " get() {\n return GWT.create(" + clientClass.getName()
+ + ".class );\n}\n}\n";
+
+ LoadStyle loadStyle = getLoadStyle(class1);
+
+ if (loadStyle != LoadStyle.EAGER) {
+ sourceWriter
+ .print("ApplicationConfiguration.startWidgetLoading();\n"
+ + "GWT.runAsync( \n"
+ + "new WidgetLoader() { void addInstantiator() {instmap.put("
+ + clientClass.getName()
+ + ".class,"
+ + instantiator + ");}});\n");
+ lazyLoadedConnectors.add(class1);
+
+ if (loadStyle == LoadStyle.DEFERRED) {
+ deferredWidgets.add(class1);
+ }
+
+ } else {
+ // widget implementation in initially loaded js script
+ sourceWriter.print("instmap.put(");
+ sourceWriter.print(clientClass.getName());
+ sourceWriter.print(".class, ");
+ sourceWriter.print(instantiator);
+ sourceWriter.print(");");
+ }
+ sourceWriter.print("}");
+ connectorsWithInstantiator.add(clientClass);
+ }
+
+ sourceWriter.println("}");
+
+ sourceWriter.println("}");
+
+ sourceWriter.println("public Class<? extends "
+ + serverConnectorClassName
+ + ">[] getDeferredLoadedConnectors() {");
+
+ sourceWriter.println("return new Class[] {");
+ first = true;
+ for (Class<?> class2 : deferredWidgets) {
+ if (!first) {
+ sourceWriter.println(",");
+ }
+ first = false;
+ sourceWriter.print(class2.getName() + ".class");
+ }
+
+ sourceWriter.println("};");
+ sourceWriter.println("}");
+
+ // in constructor add a "thread" that lazyly loads lazy loaded widgets
+ // if communication to server idles
+
+ // TODO an array of lazy loaded widgets
+
+ // TODO an index of last ensured widget in array
+
+ sourceWriter.println("public " + serverConnectorClassName
+ + " instantiate(Class<? extends " + serverConnectorClassName
+ + "> classType) {");
+ sourceWriter.indent();
+ sourceWriter.println(serverConnectorClassName
+ + " p = super.instantiate(classType); if(p!= null) return p;");
+ sourceWriter.println("return instmap.get(classType).get();");
+
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ }
+
+ /**
+ *
+ * @param sourceWriter
+ * Source writer to output source code
+ * @param paintablesHavingWidgetAnnotation
+ */
+ private void generateImplementationDetector(
+ SourceWriter sourceWriter,
+ Collection<Class<? extends ServerConnector>> paintablesHavingWidgetAnnotation) {
+ sourceWriter
+ .println("public Class<? extends "
+ + serverConnectorClassName
+ + "> "
+ + "getConnectorClassForServerSideClassName(String fullyQualifiedName) {");
+ sourceWriter.indent();
+ sourceWriter
+ .println("fullyQualifiedName = fullyQualifiedName.intern();");
+
+ for (Class<? extends ServerConnector> connectorClass : paintablesHavingWidgetAnnotation) {
+ Class<? extends ClientConnector> clientConnectorClass = getClientConnectorClass(connectorClass);
+ sourceWriter.print("if ( fullyQualifiedName == \"");
+ sourceWriter.print(clientConnectorClass.getName());
+ sourceWriter.print("\" ) { ensureInstantiator("
+ + connectorClass.getName() + ".class); return ");
+ sourceWriter.print(connectorClass.getName());
+ sourceWriter.println(".class;}");
+ sourceWriter.print("else ");
+ }
+ sourceWriter.println("return "
+ + UnknownComponentConnector.class.getName() + ".class;");
+ sourceWriter.outdent();
+ sourceWriter.println("}");
+
+ }
+
+ private static Class<? extends ClientConnector> getClientConnectorClass(
+ Class<? extends ServerConnector> connectorClass) {
+ Connect annotation = connectorClass.getAnnotation(Connect.class);
+ return (Class<? extends ClientConnector>) annotation.value();
+ }
+}
diff --git a/client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetSetBuilder.java b/client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetSetBuilder.java
new file mode 100644
index 0000000000..4c6e334a33
--- /dev/null
+++ b/client/src/com/vaadin/terminal/gwt/widgetsetutils/WidgetSetBuilder.java
@@ -0,0 +1,201 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.widgetsetutils;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to update widgetsets GWT module configuration file. Can be used
+ * command line or via IDE tools.
+ *
+ * <p>
+ * If module definition file contains text "WS Compiler: manually edited", tool
+ * will skip editing file.
+ *
+ */
+public class WidgetSetBuilder {
+
+ public static void main(String[] args) throws IOException {
+ if (args.length == 0) {
+ printUsage();
+ } else {
+ String widgetsetname = args[0];
+ updateWidgetSet(widgetsetname);
+
+ }
+ }
+
+ public static void updateWidgetSet(final String widgetset)
+ throws IOException, FileNotFoundException {
+ boolean changed = false;
+
+ Map<String, URL> availableWidgetSets = ClassPathExplorer
+ .getAvailableWidgetSets();
+
+ URL sourceUrl = availableWidgetSets.get(widgetset);
+ if (sourceUrl == null) {
+ // find first/default source directory
+ sourceUrl = ClassPathExplorer.getDefaultSourceDirectory();
+ }
+
+ String widgetsetfilename = sourceUrl.getFile() + "/"
+ + widgetset.replace(".", "/") + ".gwt.xml";
+
+ File widgetsetFile = new File(widgetsetfilename);
+ if (!widgetsetFile.exists()) {
+ // create empty gwt module file
+ File parent = widgetsetFile.getParentFile();
+ if (parent != null && !parent.exists()) {
+ if (!parent.mkdirs()) {
+ throw new IOException(
+ "Could not create directory for the widgetset: "
+ + parent.getPath());
+ }
+ }
+ widgetsetFile.createNewFile();
+ PrintStream printStream = new PrintStream(new FileOutputStream(
+ widgetsetFile));
+ printStream.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<!DOCTYPE module PUBLIC \"-//Google Inc.//DTD "
+ + "Google Web Toolkit 1.7.0//EN\" \"http://google"
+ + "-web-toolkit.googlecode.com/svn/tags/1.7.0/dis"
+ + "tro-source/core/src/gwt-module.dtd\">\n");
+ printStream.print("<module>\n");
+ printStream
+ .print(" <!--\n"
+ + " Uncomment the following to compile the widgetset for one browser only.\n"
+ + " This can reduce the GWT compilation time significantly when debugging.\n"
+ + " The line should be commented out before deployment to production\n"
+ + " environments.\n\n"
+ + " Multiple browsers can be specified for GWT 1.7 as a comma separated\n"
+ + " list. The supported user agents at the moment of writing were:\n"
+ + " ie6,ie8,gecko,gecko1_8,safari,opera\n\n"
+ + " The value gecko1_8 is used for Firefox 3 and later and safari is used for\n"
+ + " webkit based browsers including Google Chrome.\n"
+ + " -->\n"
+ + " <!-- <set-property name=\"user.agent\" value=\"gecko1_8\"/> -->\n");
+ printStream.print("\n</module>\n");
+ printStream.close();
+ changed = true;
+ }
+
+ String content = readFile(widgetsetFile);
+ if (isEditable(content)) {
+ String originalContent = content;
+
+ Collection<String> oldInheritedWidgetsets = getCurrentInheritedWidgetsets(content);
+
+ // add widgetsets that do not exist
+ Iterator<String> i = availableWidgetSets.keySet().iterator();
+ while (i.hasNext()) {
+ String ws = i.next();
+ if (ws.equals(widgetset)) {
+ // do not inherit the module itself
+ continue;
+ }
+ if (!oldInheritedWidgetsets.contains(ws)) {
+ content = addWidgetSet(ws, content);
+ }
+ }
+
+ for (String ws : oldInheritedWidgetsets) {
+ if (!availableWidgetSets.containsKey(ws)) {
+ // widgetset not available in classpath
+ content = removeWidgetSet(ws, content);
+ }
+ }
+
+ changed = changed || !content.equals(originalContent);
+ if (changed) {
+ commitChanges(widgetsetfilename, content);
+ }
+ } else {
+ System.out
+ .println("Widgetset is manually edited. Skipping updates.");
+ }
+ }
+
+ private static boolean isEditable(String content) {
+ return !content.contains("WS Compiler: manually edited");
+ }
+
+ private static String removeWidgetSet(String ws, String content) {
+ return content.replaceFirst("<inherits name=\"" + ws + "\"[^/]*/>", "");
+ }
+
+ private static void commitChanges(String widgetsetfilename, String content)
+ throws IOException {
+ BufferedWriter bufferedWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(widgetsetfilename)));
+ bufferedWriter.write(content);
+ bufferedWriter.close();
+ }
+
+ private static String addWidgetSet(String ws, String content) {
+ return content.replace("</module>", "\n <inherits name=\"" + ws
+ + "\" />" + "\n</module>");
+ }
+
+ private static Collection<String> getCurrentInheritedWidgetsets(
+ String content) {
+ HashSet<String> hashSet = new HashSet<String>();
+ Pattern inheritsPattern = Pattern.compile(" name=\"([^\"]*)\"");
+
+ Matcher matcher = inheritsPattern.matcher(content);
+
+ while (matcher.find()) {
+ String gwtModule = matcher.group(1);
+ if (isWidgetset(gwtModule)) {
+ hashSet.add(gwtModule);
+ }
+ }
+ return hashSet;
+ }
+
+ static boolean isWidgetset(String gwtModuleName) {
+ return gwtModuleName.toLowerCase().contains("widgetset");
+ }
+
+ private static String readFile(File widgetsetFile) throws IOException {
+ Reader fi = new FileReader(widgetsetFile);
+ BufferedReader bufferedReader = new BufferedReader(fi);
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(line);
+ sb.append("\n");
+ }
+ fi.close();
+ return sb.toString();
+ }
+
+ private static void printUsage() {
+ PrintStream o = System.out;
+ o.println(WidgetSetBuilder.class.getSimpleName() + " usage:");
+ o.println(" 1. Set the same classpath as you will "
+ + "have for the GWT compiler.");
+ o.println(" 2. Give the widgetsetname (to be created or updated)"
+ + " as first parameter");
+ o.println();
+ o.println("All found vaadin widgetsets will be inherited in given widgetset");
+
+ }
+
+}