summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2015-01-29 07:43:39 +0000
committerVaadin Code Review <review@vaadin.com>2015-01-29 07:43:40 +0000
commita681bf8f13163b67f55ec2d334f44f976d3eb161 (patch)
tree857a4cc6ef8910db66e256da4256c21d778b6496
parent336299a558e74e7366482640c650716880886275 (diff)
parente30b3136589799dc95049bcc87d4b7f7e707549e (diff)
downloadvaadin-framework-a681bf8f13163b67f55ec2d334f44f976d3eb161.tar.gz
vaadin-framework-a681bf8f13163b67f55ec2d334f44f976d3eb161.zip
Merge "Merge branch 'origin/grid'"
-rw-r--r--.classpath2
-rw-r--r--WebContent/VAADIN/themes/base/base.scss26
-rw-r--r--WebContent/VAADIN/themes/base/common/mixins.scss20
-rw-r--r--WebContent/VAADIN/themes/base/escalator/escalator.scss135
-rw-r--r--WebContent/VAADIN/themes/base/grid/grid.scss272
-rw-r--r--WebContent/VAADIN/themes/chameleon/chameleon.scss5
-rw-r--r--WebContent/VAADIN/themes/reindeer-tests/styles.css4
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/grid.scss60
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/img/asc-light.pngbin0 -> 228 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/img/desc-light.pngbin0 -> 231 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.pngbin0 -> 946 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/img/focus-header-bg-light.pngbin0 -> 959 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/img/focus-sel-bg-light.pngbin0 -> 954 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.pngbin0 -> 208 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gifbin0 -> 1123 bytes
-rw-r--r--WebContent/VAADIN/themes/reindeer/progressindicator/progressindicator.scss8
-rw-r--r--WebContent/VAADIN/themes/reindeer/reindeer.scss26
-rw-r--r--WebContent/VAADIN/themes/runo/grid/grid.scss53
-rw-r--r--WebContent/VAADIN/themes/runo/grid/img/header-bg.pngbin0 -> 236 bytes
-rw-r--r--WebContent/VAADIN/themes/runo/grid/img/resizer-bg.pngbin0 -> 141 bytes
-rw-r--r--WebContent/VAADIN/themes/runo/grid/img/sort-asc.pngbin0 -> 281 bytes
-rw-r--r--WebContent/VAADIN/themes/runo/grid/img/sort-desc.pngbin0 -> 303 bytes
-rw-r--r--WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gifbin0 -> 1123 bytes
-rw-r--r--WebContent/VAADIN/themes/runo/progressindicator/progressindicator.scss8
-rw-r--r--WebContent/VAADIN/themes/runo/runo.scss32
-rw-r--r--WebContent/VAADIN/themes/valo/components/_all.scss2
-rw-r--r--WebContent/VAADIN/themes/valo/components/_escalator.scss116
-rw-r--r--WebContent/VAADIN/themes/valo/components/_grid.scss96
-rw-r--r--all/build.xml106
-rw-r--r--all/ivy.xml1
-rw-r--r--buildhelpers/build.xml126
-rw-r--r--buildhelpers/ivy.xml4
-rw-r--r--client-compiled/build.xml41
-rw-r--r--client-compiler/build.xml13
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java83
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java14
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java6
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java62
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java10
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java6
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java111
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java20
-rw-r--r--client/build.xml15
-rw-r--r--client/ivy.xml3
-rwxr-xr-xclient/src/com/vaadin/DefaultWidgetSet.gwt.xml15
-rw-r--r--client/src/com/vaadin/Vaadin.gwt.xml18
-rw-r--r--client/src/com/vaadin/client/ApplicationConfiguration.java4
-rw-r--r--client/src/com/vaadin/client/ApplicationConnection.java195
-rw-r--r--client/src/com/vaadin/client/BrowserInfo.java5
-rw-r--r--client/src/com/vaadin/client/JavaScriptConnectorHelper.java129
-rw-r--r--client/src/com/vaadin/client/LayoutManager.java15
-rw-r--r--client/src/com/vaadin/client/LayoutManagerIE8.java2
-rw-r--r--client/src/com/vaadin/client/MeasuredSize.java4
-rw-r--r--client/src/com/vaadin/client/MouseEventDetailsBuilder.java4
-rw-r--r--client/src/com/vaadin/client/Profiler.java234
-rw-r--r--client/src/com/vaadin/client/RenderSpace.java2
-rw-r--r--client/src/com/vaadin/client/ResourceLoader.java6
-rw-r--r--client/src/com/vaadin/client/StyleConstants.java9
-rw-r--r--client/src/com/vaadin/client/SuperDevMode.java2
-rw-r--r--client/src/com/vaadin/client/Util.java1080
-rw-r--r--client/src/com/vaadin/client/VCaption.java40
-rw-r--r--client/src/com/vaadin/client/VLoadingIndicator.java12
-rw-r--r--client/src/com/vaadin/client/VUIDLBrowser.java28
-rw-r--r--client/src/com/vaadin/client/WidgetUtil.java1405
-rw-r--r--client/src/com/vaadin/client/communication/AtmospherePushConnection.java17
-rw-r--r--client/src/com/vaadin/client/communication/Date_Serializer.java13
-rw-r--r--client/src/com/vaadin/client/communication/DiffJSONSerializer.java4
-rw-r--r--client/src/com/vaadin/client/communication/JSONSerializer.java10
-rw-r--r--client/src/com/vaadin/client/communication/JsonDecoder.java130
-rw-r--r--client/src/com/vaadin/client/communication/JsonEncoder.java88
-rw-r--r--client/src/com/vaadin/client/communication/PushConnection.java4
-rw-r--r--client/src/com/vaadin/client/communication/RpcManager.java35
-rw-r--r--client/src/com/vaadin/client/communication/StateChangeEvent.java20
-rw-r--r--client/src/com/vaadin/client/communication/URLReference_Serializer.java18
-rw-r--r--client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java7
-rw-r--r--client/src/com/vaadin/client/connectors/AbstractRendererConnector.java182
-rw-r--r--client/src/com/vaadin/client/connectors/ButtonRendererConnector.java44
-rw-r--r--client/src/com/vaadin/client/connectors/ClickableRendererConnector.java61
-rw-r--r--client/src/com/vaadin/client/connectors/DateRendererConnector.java34
-rw-r--r--client/src/com/vaadin/client/connectors/GridConnector.java964
-rw-r--r--client/src/com/vaadin/client/connectors/ImageRendererConnector.java55
-rw-r--r--client/src/com/vaadin/client/connectors/JavaScriptRendererConnector.java280
-rw-r--r--client/src/com/vaadin/client/connectors/NumberRendererConnector.java35
-rw-r--r--client/src/com/vaadin/client/connectors/ObjectRendererConnector.java38
-rw-r--r--client/src/com/vaadin/client/connectors/ProgressBarRendererConnector.java35
-rw-r--r--client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java183
-rw-r--r--client/src/com/vaadin/client/connectors/TextRendererConnector.java34
-rw-r--r--client/src/com/vaadin/client/connectors/UnsafeHtmlRendererConnector.java43
-rw-r--r--client/src/com/vaadin/client/data/AbstractRemoteDataSource.java714
-rw-r--r--client/src/com/vaadin/client/data/CacheStrategy.java183
-rw-r--r--client/src/com/vaadin/client/data/DataChangeHandler.java82
-rw-r--r--client/src/com/vaadin/client/data/DataSource.java209
-rw-r--r--client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java14
-rw-r--r--client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java11
-rw-r--r--client/src/com/vaadin/client/debug/internal/HierarchySection.java5
-rw-r--r--client/src/com/vaadin/client/debug/internal/ProfilerSection.java235
-rw-r--r--client/src/com/vaadin/client/debug/internal/TestBenchSection.java11
-rw-r--r--client/src/com/vaadin/client/extensions/ResponsiveConnector.java2
-rw-r--r--client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java4
-rw-r--r--client/src/com/vaadin/client/metadata/Method.java14
-rw-r--r--client/src/com/vaadin/client/metadata/Property.java13
-rw-r--r--client/src/com/vaadin/client/metadata/TypeDataStore.java86
-rw-r--r--client/src/com/vaadin/client/renderers/ButtonRenderer.java44
-rw-r--r--client/src/com/vaadin/client/renderers/ClickableRenderer.java229
-rw-r--r--client/src/com/vaadin/client/renderers/ComplexRenderer.java157
-rw-r--r--client/src/com/vaadin/client/renderers/DateRenderer.java108
-rw-r--r--client/src/com/vaadin/client/renderers/HtmlRenderer.java41
-rw-r--r--client/src/com/vaadin/client/renderers/ImageRenderer.java43
-rw-r--r--client/src/com/vaadin/client/renderers/NumberRenderer.java71
-rw-r--r--client/src/com/vaadin/client/renderers/ObjectRenderer.java36
-rw-r--r--client/src/com/vaadin/client/renderers/ProgressBarRenderer.java47
-rw-r--r--client/src/com/vaadin/client/renderers/Renderer.java48
-rw-r--r--client/src/com/vaadin/client/renderers/TextRenderer.java32
-rw-r--r--client/src/com/vaadin/client/renderers/WidgetRenderer.java104
-rw-r--r--client/src/com/vaadin/client/ui/AbstractClickEventHandler.java11
-rw-r--r--client/src/com/vaadin/client/ui/AbstractComponentConnector.java13
-rw-r--r--client/src/com/vaadin/client/ui/AbstractConnector.java14
-rw-r--r--client/src/com/vaadin/client/ui/AbstractFieldConnector.java10
-rw-r--r--client/src/com/vaadin/client/ui/MediaBaseConnector.java4
-rw-r--r--client/src/com/vaadin/client/ui/SubPartAware.java3
-rw-r--r--client/src/com/vaadin/client/ui/VAbstractSplitPanel.java14
-rw-r--r--client/src/com/vaadin/client/ui/VAccordion.java4
-rw-r--r--client/src/com/vaadin/client/ui/VButton.java5
-rw-r--r--client/src/com/vaadin/client/ui/VCalendarPanel.java4
-rw-r--r--client/src/com/vaadin/client/ui/VContextMenu.java8
-rw-r--r--client/src/com/vaadin/client/ui/VCustomLayout.java4
-rw-r--r--client/src/com/vaadin/client/ui/VEmbedded.java41
-rw-r--r--client/src/com/vaadin/client/ui/VFilterSelect.java32
-rw-r--r--client/src/com/vaadin/client/ui/VFlash.java37
-rw-r--r--client/src/com/vaadin/client/ui/VFormLayout.java5
-rw-r--r--client/src/com/vaadin/client/ui/VLabel.java3
-rw-r--r--client/src/com/vaadin/client/ui/VMenuBar.java10
-rw-r--r--client/src/com/vaadin/client/ui/VNativeButton.java3
-rw-r--r--client/src/com/vaadin/client/ui/VNotification.java8
-rw-r--r--client/src/com/vaadin/client/ui/VOptionGroup.java16
-rw-r--r--client/src/com/vaadin/client/ui/VOverlay.java3
-rw-r--r--client/src/com/vaadin/client/ui/VPopupView.java9
-rw-r--r--client/src/com/vaadin/client/ui/VProgressBar.java14
-rw-r--r--client/src/com/vaadin/client/ui/VScrollTable.java80
-rw-r--r--client/src/com/vaadin/client/ui/VSlider.java8
-rw-r--r--client/src/com/vaadin/client/ui/VTabsheet.java6
-rw-r--r--client/src/com/vaadin/client/ui/VTextArea.java4
-rw-r--r--client/src/com/vaadin/client/ui/VTextField.java4
-rw-r--r--client/src/com/vaadin/client/ui/VTree.java7
-rw-r--r--client/src/com/vaadin/client/ui/VTreeTable.java7
-rw-r--r--client/src/com/vaadin/client/ui/VTwinColSelect.java18
-rw-r--r--client/src/com/vaadin/client/ui/VUI.java4
-rw-r--r--client/src/com/vaadin/client/ui/VUpload.java4
-rw-r--r--client/src/com/vaadin/client/ui/VWindow.java31
-rw-r--r--client/src/com/vaadin/client/ui/accordion/AccordionConnector.java5
-rw-r--r--client/src/com/vaadin/client/ui/calendar/CalendarConnector.java4
-rw-r--r--client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java6
-rw-r--r--client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java4
-rw-r--r--client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java4
-rw-r--r--client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java6
-rw-r--r--client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java4
-rw-r--r--client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java8
-rw-r--r--client/src/com/vaadin/client/ui/dd/DDUtil.java6
-rw-r--r--client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java24
-rw-r--r--client/src/com/vaadin/client/ui/dd/VDragEvent.java6
-rw-r--r--client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java8
-rw-r--r--client/src/com/vaadin/client/ui/label/LabelConnector.java4
-rw-r--r--client/src/com/vaadin/client/ui/layout/LayoutDependencyTree.java2
-rw-r--r--client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java4
-rw-r--r--client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java3
-rw-r--r--client/src/com/vaadin/client/ui/orderedlayout/Slot.java12
-rw-r--r--client/src/com/vaadin/client/ui/orderedlayout/VAbstractOrderedLayout.java3
-rw-r--r--client/src/com/vaadin/client/ui/table/TableConnector.java6
-rw-r--r--client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java5
-rw-r--r--client/src/com/vaadin/client/ui/textarea/TextAreaConnector.java2
-rw-r--r--client/src/com/vaadin/client/ui/tree/TreeConnector.java6
-rw-r--r--client/src/com/vaadin/client/ui/treetable/TreeTableConnector.java4
-rw-r--r--client/src/com/vaadin/client/ui/window/WindowConnector.java4
-rw-r--r--client/src/com/vaadin/client/widget/escalator/Cell.java85
-rw-r--r--client/src/com/vaadin/client/widget/escalator/ColumnConfiguration.java179
-rw-r--r--client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java157
-rw-r--r--client/src/com/vaadin/client/widget/escalator/FlyweightCell.java201
-rw-r--r--client/src/com/vaadin/client/widget/escalator/FlyweightRow.java295
-rw-r--r--client/src/com/vaadin/client/widget/escalator/PositionFunction.java118
-rw-r--r--client/src/com/vaadin/client/widget/escalator/Row.java49
-rw-r--r--client/src/com/vaadin/client/widget/escalator/RowContainer.java196
-rw-r--r--client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java90
-rw-r--r--client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeHandler.java38
-rw-r--r--client/src/com/vaadin/client/widget/escalator/ScrollbarBundle.java854
-rw-r--r--client/src/com/vaadin/client/widget/grid/CellReference.java128
-rw-r--r--client/src/com/vaadin/client/widget/grid/CellStyleGenerator.java40
-rw-r--r--client/src/com/vaadin/client/widget/grid/DataAvailableEvent.java55
-rw-r--r--client/src/com/vaadin/client/widget/grid/DataAvailableHandler.java37
-rw-r--r--client/src/com/vaadin/client/widget/grid/EditorHandler.java243
-rw-r--r--client/src/com/vaadin/client/widget/grid/EventCellReference.java64
-rw-r--r--client/src/com/vaadin/client/widget/grid/RendererCellReference.java89
-rw-r--r--client/src/com/vaadin/client/widget/grid/RowReference.java104
-rw-r--r--client/src/com/vaadin/client/widget/grid/RowStyleGenerator.java40
-rw-r--r--client/src/com/vaadin/client/widget/grid/datasources/ListDataSource.java464
-rw-r--r--client/src/com/vaadin/client/widget/grid/datasources/ListSorter.java175
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/AbstractGridKeyEventHandler.java44
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/AbstractGridMouseEventHandler.java39
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/BodyClickHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/BodyDoubleClickHandler.java29
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/BodyKeyDownHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/BodyKeyPressHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/BodyKeyUpHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/FooterClickHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/FooterDoubleClickHandler.java29
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/FooterKeyDownHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/FooterKeyPressHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/FooterKeyUpHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/GridClickEvent.java50
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/GridDoubleClickEvent.java52
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/GridKeyDownEvent.java121
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/GridKeyPressEvent.java74
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/GridKeyUpEvent.java121
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/HeaderClickHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/HeaderDoubleClickHandler.java29
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/HeaderKeyDownHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/HeaderKeyPressHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/HeaderKeyUpHandler.java28
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/ScrollEvent.java40
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/ScrollHandler.java35
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/SelectAllEvent.java59
-rw-r--r--client/src/com/vaadin/client/widget/grid/events/SelectAllHandler.java37
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/AbstractRowHandleSelectionModel.java66
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/ClickSelectHandler.java63
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/HasSelectionHandlers.java42
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java719
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SelectionEvent.java178
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SelectionHandler.java39
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SelectionModel.java238
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SelectionModelMulti.java273
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SelectionModelNone.java73
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SelectionModelSingle.java151
-rw-r--r--client/src/com/vaadin/client/widget/grid/selection/SpaceSelectHandler.java124
-rw-r--r--client/src/com/vaadin/client/widget/grid/sort/Sort.java154
-rw-r--r--client/src/com/vaadin/client/widget/grid/sort/SortEvent.java113
-rw-r--r--client/src/com/vaadin/client/widget/grid/sort/SortHandler.java38
-rw-r--r--client/src/com/vaadin/client/widget/grid/sort/SortOrder.java90
-rw-r--r--client/src/com/vaadin/client/widgets/Escalator.java5197
-rw-r--r--client/src/com/vaadin/client/widgets/Grid.java5867
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java192
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java104
-rw-r--r--common.xml157
-rw-r--r--ivysettings.xml2
-rw-r--r--push/build.xml26
-rw-r--r--server/build.xml12
-rw-r--r--server/src/com/vaadin/data/Container.java58
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java1045
-rw-r--r--server/src/com/vaadin/data/fieldgroup/FieldGroup.java3
-rw-r--r--server/src/com/vaadin/data/sort/Sort.java153
-rw-r--r--server/src/com/vaadin/data/sort/SortOrder.java106
-rw-r--r--server/src/com/vaadin/data/util/AbstractBeanContainer.java20
-rw-r--r--server/src/com/vaadin/data/util/AbstractInMemoryContainer.java155
-rw-r--r--server/src/com/vaadin/data/util/GeneratedPropertyContainer.java724
-rw-r--r--server/src/com/vaadin/data/util/IndexedContainer.java34
-rw-r--r--server/src/com/vaadin/data/util/PropertyValueGenerator.java100
-rw-r--r--server/src/com/vaadin/event/SelectionEvent.java114
-rw-r--r--server/src/com/vaadin/event/SortEvent.java110
-rw-r--r--server/src/com/vaadin/server/AbstractJavaScriptExtension.java4
-rw-r--r--server/src/com/vaadin/server/JsonCodec.java9
-rw-r--r--server/src/com/vaadin/ui/AbstractJavaScriptComponent.java4
-rw-r--r--server/src/com/vaadin/ui/DefaultFieldFactory.java38
-rw-r--r--server/src/com/vaadin/ui/Grid.java4678
-rw-r--r--server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java157
-rw-r--r--server/src/com/vaadin/ui/renderer/ButtonRenderer.java45
-rw-r--r--server/src/com/vaadin/ui/renderer/ClickableRenderer.java138
-rw-r--r--server/src/com/vaadin/ui/renderer/DateRenderer.java156
-rw-r--r--server/src/com/vaadin/ui/renderer/HtmlRenderer.java33
-rw-r--r--server/src/com/vaadin/ui/renderer/ImageRenderer.java67
-rw-r--r--server/src/com/vaadin/ui/renderer/NumberRenderer.java163
-rw-r--r--server/src/com/vaadin/ui/renderer/ObjectRenderer.java46
-rw-r--r--server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java44
-rw-r--r--server/src/com/vaadin/ui/renderer/Renderer.java69
-rw-r--r--server/src/com/vaadin/ui/renderer/TextRenderer.java34
-rw-r--r--server/src/com/vaadin/ui/themes/Reindeer.java20
-rw-r--r--server/src/com/vaadin/ui/themes/Runo.java12
-rw-r--r--server/tests/src/com/vaadin/data/fieldgroup/FieldGroupTests.java8
-rw-r--r--server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java184
-rw-r--r--server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java306
-rw-r--r--server/tests/src/com/vaadin/data/util/IndexedContainerTest.java144
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java88
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java219
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java134
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java254
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java103
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridEditorTest.java287
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java301
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java132
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java171
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java154
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java203
-rw-r--r--server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java85
-rw-r--r--server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java222
-rw-r--r--shared/build.xml14
-rw-r--r--shared/src/com/vaadin/shared/AbstractComponentState.java3
-rw-r--r--shared/src/com/vaadin/shared/annotations/NoLayout.java43
-rw-r--r--shared/src/com/vaadin/shared/annotations/NoLoadingIndicator.java35
-rw-r--r--shared/src/com/vaadin/shared/communication/SharedState.java2
-rw-r--r--shared/src/com/vaadin/shared/data/DataProviderRpc.java94
-rw-r--r--shared/src/com/vaadin/shared/data/DataRequestRpc.java59
-rw-r--r--shared/src/com/vaadin/shared/data/sort/SortDirection.java54
-rw-r--r--shared/src/com/vaadin/shared/ui/AbstractEmbeddedState.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/AbstractMediaState.java4
-rw-r--r--shared/src/com/vaadin/shared/ui/MediaControl.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/TabIndexState.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/button/ButtonState.java4
-rw-r--r--shared/src/com/vaadin/shared/ui/datefield/PopupDateFieldState.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/datefield/TextualDateFieldState.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java45
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/EditorClientRpc.java61
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/EditorServerRpc.java58
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java53
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridColumnState.java73
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridConstants.java71
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java50
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridState.java149
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java39
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java68
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/HeightMode.java42
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/Range.java439
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java55
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/renderers/RendererClickRpc.java31
-rw-r--r--shared/src/com/vaadin/shared/ui/panel/PanelState.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/popupview/PopupViewState.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/progressindicator/ProgressBarState.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorServerRpc.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorState.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/slider/SliderState.java5
-rw-r--r--shared/src/com/vaadin/shared/ui/tabsheet/TabsheetState.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/textarea/TextAreaState.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/textfield/AbstractTextFieldState.java4
-rw-r--r--shared/src/com/vaadin/shared/ui/ui/ScrollClientRpc.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/ui/UIServerRpc.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/window/WindowState.java15
-rw-r--r--shared/src/com/vaadin/shared/util/SharedUtil.java142
-rw-r--r--shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java425
-rw-r--r--shared/tests/src/com/vaadin/shared/util/SharedUtilTests.java66
-rw-r--r--themes/build.xml28
-rw-r--r--uitest/build.xml132
-rw-r--r--uitest/src/com/vaadin/testbench/elements/GridElement.java325
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/AbstractGridColumnAutoWidthTest.java109
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java76
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java63
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInit.java62
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInitTest.java46
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridAddRow.java49
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridAddRowTest.java50
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java292
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColspans.java102
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java101
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidth.java63
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClient.java33
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClientTest.java27
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthServerTest.java27
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColumnExpand.java159
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridEditorUI.java49
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridEditorUITest.java63
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridGeneratedProperties.java165
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridGeneratedPropertiesTest.java90
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNames.java111
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNamesTest.java158
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridInTabSheet.java69
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridInTabSheetTest.java61
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInit.java34
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInitTest.java35
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridScrolling.java112
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java60
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java47
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridThemeChange.java59
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java51
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverter.java37
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverterTest.java25
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridWithoutRenderer.java34
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridWithoutRendererTest.java43
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumns.java41
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumnsTest.java42
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java24
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderers.java75
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderersTest.java46
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/MyBeanJSRenderer.java34
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java33
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/SelectDuringInit.java40
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/SelectDuringInitTest.java35
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/WidgetRenderers.java117
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/WidgetRenderersTest.java132
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeatures.java36
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java262
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorUpdaterUi.java33
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java39
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java92
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java1026
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java135
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesValo.java28
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java32
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java180
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInit.java20
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInitTest.java19
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRenderer.java32
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRendererTest.java68
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInit.java50
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInitTest.java20
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicators.java65
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicatorsTest.java39
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/DisabledGridClientTest.java58
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridCellStyleGeneratorTest.java131
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java128
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeEditorTest.java13
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeFooterTest.java11
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeHeaderTest.java11
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeKeyEventsTest.java12
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeSelectionTest.java11
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java132
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java102
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientStructureTest.java37
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridEditorClientTest.java155
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java219
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java281
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridRowHandleRefreshTest.java65
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java90
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java114
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java64
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColspanTest.java80
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColumnFreezingTest.java111
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorRowColumnTest.java316
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorScrollTest.java95
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorUpdaterUiTest.java151
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/DisabledGridTest.java58
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellFocusAdjustmentTest.java86
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellStyleGeneratorTest.java121
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridEditorTest.java259
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridItemClickTest.java57
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java221
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridMultiSortingTest.java70
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridRowAddRemoveTest.java57
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java292
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java375
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java71
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java485
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java86
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/myBeanJsRenderer.js16
-rw-r--r--uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeer.java32
-rw-r--r--uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeerTest.java28
-rw-r--r--uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRuno.java32
-rw-r--r--uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRunoTest.java28
-rw-r--r--uitest/src/com/vaadin/tests/serialization/NoLayout.java101
-rw-r--r--uitest/src/com/vaadin/tests/serialization/NoLayoutTest.java84
-rw-r--r--uitest/src/com/vaadin/tests/serialization/SerializerTest.java21
-rw-r--r--uitest/src/com/vaadin/tests/serialization/SerializerTestTest.java6
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml6
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/BasicExtensionTestConnector.java5
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/CustomUIConnector.java3
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/LayoutDetectorConnector.java61
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java11
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/NoLayoutRpc.java26
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/RoundTripTesterRpc.java2
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/SerializerTestConnector.java33
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/SerializerTestRpc.java5
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/SerializerTestState.java7
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/TestWidgetConnector.java100
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java702
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java218
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java1173
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java386
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java48
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java219
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridColumnAutoWidthClientWidget.java71
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridDefaultTextRendererWidget.java64
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridHeightByRowOnInitWidget.java32
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java46
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java308
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java78
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java144
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/LayoutDetector.java26
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/TestWidgetComponent.java67
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java158
-rw-r--r--widgets/build.xml128
-rw-r--r--widgets/ivy.xml48
-rw-r--r--widgets/src/com/vaadin/themes/Valo.gwt.xml9
-rw-r--r--widgets/src/com/vaadin/themes/valoutil/BodyStyleName.java13
477 files changed, 55088 insertions, 2369 deletions
diff --git a/.classpath b/.classpath
index 847fe8f769..517b236661 100644
--- a/.classpath
+++ b/.classpath
@@ -10,6 +10,7 @@
<classpathentry kind="src" path="uitest/src"/>
<classpathentry kind="src" path="buildhelpers/src"/>
<classpathentry kind="src" path="shared/src"/>
+ <classpathentry kind="src" path="widgets/src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6">
<attributes>
<attribute name="owner.project.facets" value="java"/>
@@ -25,5 +26,6 @@
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/>
<classpathentry kind="con" path="org.apache.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/?project=vaadin&amp;ivyXmlPath=buildhelpers%2Fivy.xml&amp;confs=ide&amp;ivySettingsPath=%24%7Bworkspace_loc%3Avaadin%2Fivysettings.xml%7D&amp;loadSettingsOnDemand=false&amp;propertyFiles=build.properties"/>
<classpathentry exported="true" kind="con" path="org.apache.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/?project=vaadin&amp;ivyXmlPath=gwt%2Fivy.xml&amp;confs=ide&amp;ivySettingsPath=%24%7Bworkspace_loc%3Avaadin%2Fivysettings.xml%7D&amp;loadSettingsOnDemand=false&amp;propertyFiles=%24%7Bworkspace_loc%3Avaadin%2Fbuild.properties%7D"/>
+ <classpathentry kind="con" path="org.apache.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/?project=vaadin&amp;ivyXmlPath=widgets%2Fivy.xml&amp;confs=ide&amp;ivySettingsPath=%24%7Bworkspace_loc%3Avaadin%2Fivysettings.xml%7D&amp;loadSettingsOnDemand=false&amp;propertyFiles=build.properties"/>
<classpathentry kind="output" path="build/classes"/>
</classpath>
diff --git a/WebContent/VAADIN/themes/base/base.scss b/WebContent/VAADIN/themes/base/base.scss
index 3570c5efec..d40ac1a7bf 100644
--- a/WebContent/VAADIN/themes/base/base.scss
+++ b/WebContent/VAADIN/themes/base/base.scss
@@ -1,3 +1,11 @@
+$font-size: 16px !default;
+$line-height: normal !default;
+
+// Provide these so that we can use them in base mixins
+// and so that we can use base mixins in Valo
+$v-font-size: $font-size !default;
+$v-line-height: $line-height !default;
+
@import "common/mixins.scss";
@import "absolutelayout/absolutelayout.scss";
@import "accordion/accordion.scss";
@@ -16,8 +24,10 @@
@import "inlinedatefield/inlinedatefield.scss";
@import "dragwrapper/dragwrapper.scss";
@import "embedded/embedded.scss";
+@import "escalator/escalator.scss";
@import "fonts/fonts.scss";
@import "formlayout/formlayout.scss";
+@import "grid/grid.scss";
@import "gridlayout/gridlayout.scss";
@import "label/label.scss";
@import "link/link.scss";
@@ -59,16 +69,14 @@
overflow: hidden;
}
-$font-size: 16px;
-$line-height: normal;
@mixin base {
// @include base-app;
-
+
// everything included from base theme
// other themes should enclose corresponding definitions in theme selectors
-
+
@include base-widget;
-
+
@include base-absolutelayout;
@include base-accordion;
@include base-browserframe;
@@ -78,10 +86,10 @@ $line-height: normal;
@include base-caption;
@include base-colorpicker;
@include base-calendar;
-
+
// here for now to preserve old semantics
@include base-common;
-
+
@include base-layout;
@include base-csslayout;
@include base-customcomponent;
@@ -90,7 +98,9 @@ $line-height: normal;
@include base-inline-datefield;
@include base-dragwrapper;
@include base-embedded;
+ @include base-escalator;
@include base-formlayout;
+ @include base-grid;
@include base-gridlayout;
@include base-label;
@include base-link;
@@ -103,7 +113,7 @@ $line-height: normal;
@include base-progressindicator(v-progressbar);
/* For legacy ProgressIndicator component */
@include base-progressindicator(v-progressindicator);
-
+
@include base-select;
@include base-shadow;
@include base-slider;
diff --git a/WebContent/VAADIN/themes/base/common/mixins.scss b/WebContent/VAADIN/themes/base/common/mixins.scss
index 79d26d6c16..fab97e9565 100644
--- a/WebContent/VAADIN/themes/base/common/mixins.scss
+++ b/WebContent/VAADIN/themes/base/common/mixins.scss
@@ -1,5 +1,5 @@
@mixin keyframes ($name) {
- @-webkit-keyframes #{$name} {
+ @-webkit-keyframes #{$name} {
@content;
}
@-moz-keyframes #{$name} {
@@ -11,7 +11,19 @@
}
@mixin animation ($anim) {
- -webkit-animation: $anim;
- -moz-animation: $anim;
- animation: $anim;
+ -webkit-animation: $anim;
+ -moz-animation: $anim;
+ animation: $anim;
+}
+
+@mixin box-shadow ($shadow) {
+ -webkit-box-shadow: $shadow;
+ -moz-box-shadow: $shadow;
+ box-shadow: $shadow;
+}
+
+@mixin box-sizing ($box-sizing) {
+ -webkit-box-sizing: $box-sizing;
+ -moz-box-sizing: $box-sizing;
+ box-sizing: $box-sizing;
}
diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss
new file mode 100644
index 0000000000..ad09207ce0
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss
@@ -0,0 +1,135 @@
+@mixin base-escalator($primaryStyleName: v-escalator, $background-color: #fff) {
+
+ .#{$primaryStyleName} {
+ position: relative;
+ }
+
+ .#{$primaryStyleName}-scroller {
+ position: absolute;
+ z-index: 20;
+ outline: none;
+ @include box-sizing(border-box);
+ }
+
+ .#{$primaryStyleName}-scroller-horizontal {
+ left: 0; // Left position adjusted to align with frozen columns
+ right: 0;
+ bottom: 0;
+ overflow-y: hidden;
+ -ms-overflow-y: hidden;
+ }
+
+ .#{$primaryStyleName}-scroller-vertical {
+ right: 0;
+ top: 0; // this will be overridden by code, but it's a good default behavior
+ bottom: 0; // this will be overridden by code, but it's a good default behavior
+ overflow-x: hidden;
+ -ms-overflow-x: hidden;
+ }
+
+ .#{$primaryStyleName}-tablewrapper {
+ position: absolute;
+ overflow: hidden;
+ @include box-sizing(border-box);
+ }
+
+ .#{$primaryStyleName}-tablewrapper > table {
+ border-spacing: 0;
+ table-layout: fixed;
+ width: inherit; // a decent default fallback
+ }
+
+ .#{$primaryStyleName}-header-deco,
+ .#{$primaryStyleName}-footer-deco {
+ position: absolute;
+ right: 0;
+ @include box-sizing(border-box);
+ }
+
+ .#{$primaryStyleName}-horizontal-scrollbar-deco {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ @include box-sizing(border-box);
+ }
+
+ .#{$primaryStyleName}-header,
+ .#{$primaryStyleName}-body,
+ .#{$primaryStyleName}-footer {
+ position: absolute;
+ left: 0;
+ width: inherit;
+ z-index: 10;
+ }
+
+ .#{$primaryStyleName}-header,
+ .#{$primaryStyleName}-header-deco {
+ top: 0;
+ }
+
+ .#{$primaryStyleName}-footer,
+ .#{$primaryStyleName}-footer-deco {
+ bottom: 0;
+ }
+
+ .#{$primaryStyleName}-body {
+ z-index: 0;
+ top: 0;
+
+ .#{$primaryStyleName}-row {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ }
+
+ .#{$primaryStyleName}-row {
+ display: block;
+
+ .v-ie8 &, .v-ie9 & {
+ // Neither IE8 nor IE9 let table rows be longer than tbody, with only
+ // "display: block". Moar hax.
+
+ float: left;
+ clear: left;
+
+ // The inline style of margin-top from the <tbody> to offset the
+ // header's dimension is, for some strange reason, inherited into each
+ // contained <tr>. We need to cancel it:
+
+ margin-top: 0;
+ }
+
+ > td,
+ > th {
+ // IE8 likes the bgcolor here instead of on the row
+ background-color: $background-color;
+ }
+ }
+
+ .#{$primaryStyleName}-row {
+ width: inherit;
+ }
+
+ .#{$primaryStyleName}-cell {
+ display: block;
+ float: left;
+ padding: 2px;
+ white-space: nowrap;
+ @include box-sizing(border-box);
+ overflow: hidden;
+
+ // Because Vaadin changes the font size after the initial render, we
+ // need to mention the font size here explicitly, otherwise automatic
+ // row height detection gets broken.
+
+ font-size: $v-font-size;
+ }
+
+ .#{$primaryStyleName}-cell.frozen {
+ position: relative;
+ z-index: 1;
+ }
+
+}
diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss
new file mode 100644
index 0000000000..ed068a5efc
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/grid/grid.scss
@@ -0,0 +1,272 @@
+$v-grid-border: 1px solid #ddd !default;
+$v-grid-cell-vertical-border: $v-grid-border !default;
+$v-grid-cell-horizontal-border: $v-grid-cell-vertical-border !default;
+$v-grid-cell-focused-border: 1px solid !default;
+$v-grid-header-border: $v-grid-border !default;
+$v-grid-footer-border: $v-grid-header-border !default;
+
+$v-grid-row-height: round($v-font-size * 1.5) !default;
+$v-grid-row-background-color: #fff !default;
+$v-grid-row-stripe-background-color: darken($v-grid-row-background-color, 5%) !default;
+$v-grid-row-selected-background-color: darken($v-grid-row-background-color, 25%) !default;
+$v-grid-row-focused-background-color: null !default;
+
+$v-grid-header-row-height: null !default;
+$v-grid-header-font-size: $v-font-size !default;
+$v-grid-header-background-color: $v-grid-row-background-color !default;
+
+$v-grid-footer-row-height: $v-grid-header-row-height !default;
+$v-grid-footer-font-size: $v-grid-header-font-size !default;
+$v-grid-footer-background-color: $v-grid-header-background-color !default;
+
+$v-grid-cell-padding-horizontal: 5px !default;
+
+$v-grid-editor-background-color: $v-grid-row-background-color !default;
+
+
+@import "../escalator/escalator";
+
+
+@mixin base-grid($primaryStyleName: v-grid) {
+
+ @include base-escalator($primaryStyleName: $primaryStyleName, $background-color: $v-grid-row-background-color);
+
+ .#{$primaryStyleName} {
+ outline: none;
+ }
+
+ .#{$primaryStyleName}-scroller-vertical,
+ .#{$primaryStyleName}-scroller-horizontal {
+ border: $v-grid-border;
+ }
+
+ .#{$primaryStyleName}-scroller-vertical {
+ border-left: none;
+ }
+
+ .#{$primaryStyleName}-scroller-horizontal {
+ border-top: none;
+ }
+
+ .#{$primaryStyleName}-tablewrapper {
+ border: $v-grid-border;
+ }
+
+ // Common cell styles
+
+ .#{$primaryStyleName}-cell {
+ background-color: $v-grid-row-background-color;
+ padding: 0 $v-grid-cell-padding-horizontal;
+ line-height: $v-grid-row-height;
+ text-overflow: ellipsis;
+
+ > * {
+ line-height: $v-line-height;
+ vertical-align: middle;
+ }
+
+ // Force div elements to inline-blocks by default to enable vertical centering
+ > div {
+ display: inline-block;
+ }
+
+ &.frozen {
+ @include box-shadow(1px 0 2px rgba(0,0,0,.1));
+ border-right: $v-grid-cell-vertical-border;
+
+ @if $v-grid-cell-vertical-border and $v-grid-cell-vertical-border != none {
+ + th,
+ + td {
+ border-left: none;
+ }
+ }
+ }
+ }
+
+ // Rows
+
+ .#{$primaryStyleName}-row > td {
+ border-left: $v-grid-cell-vertical-border;
+ border-bottom: $v-grid-cell-horizontal-border;
+
+ &:first-child {
+ border-left: none;
+ }
+ }
+
+ .#{$primaryStyleName}-row-stripe > td {
+ background-color: $v-grid-row-stripe-background-color;
+ }
+
+ .#{$primaryStyleName}-row-selected > td {
+ background: $v-grid-row-selected-background-color;
+ }
+
+ .#{$primaryStyleName}-row-focused > td {
+ background-color: $v-grid-row-focused-background-color;
+ }
+
+ // Header
+
+ .#{$primaryStyleName}-header {
+ th {
+ position: relative;
+ background-color: $v-grid-header-background-color;
+ font-size: $v-grid-header-font-size;
+ font-weight: inherit;
+ border-left: $v-grid-header-border;
+ border-bottom: $v-grid-header-border;
+ line-height: $v-grid-header-row-height;
+ text-align: left;
+
+ &:first-child {
+ border-left: none;
+ }
+ }
+
+ .sort-asc,
+ .sort-desc {
+ padding-right: round($v-grid-header-font-size * 1.2) + $v-grid-cell-padding-horizontal;
+
+ &:after {
+ font-family: FontAwesome, sans-serif;
+ content: "\f0de" " " attr(sort-order);
+ position: absolute;
+ right: $v-grid-cell-padding-horizontal;
+ font-size: round($v-grid-header-font-size * 0.85);
+ }
+ }
+
+ .sort-desc:after {
+ content: "\f0dd" " " attr(sort-order);
+ }
+ }
+
+ // Footer
+
+ .#{$primaryStyleName}-footer {
+ td {
+ background-color: $v-grid-footer-background-color;
+ font-size: $v-grid-footer-font-size;
+ font-weight: inherit;
+ border-left: $v-grid-footer-border;
+ border-top: $v-grid-footer-border;
+ border-bottom: none;
+ line-height: $v-grid-footer-row-height;
+
+ &:first-child {
+ border-left: none;
+ }
+ }
+ }
+
+ // Decorative elements
+
+ .#{$primaryStyleName}-header-deco {
+ border-top: $v-grid-header-border;
+ border-right: $v-grid-header-border;
+ background-color: $v-grid-header-background-color;
+ }
+
+ .#{$primaryStyleName}-footer-deco {
+ border-bottom: $v-grid-footer-border;
+ border-right: $v-grid-footer-border;
+ background-color: $v-grid-footer-background-color;
+ }
+
+ .#{$primaryStyleName}-horizontal-scrollbar-deco {
+ background-color: $v-grid-footer-background-color;
+ border: $v-grid-footer-border;
+ border-top: none;
+ }
+
+ // Focused cell style (common for all cells)
+
+ .#{$primaryStyleName}-cell-focused {
+ position: relative;
+
+ &:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ border: $v-grid-cell-focused-border;
+ display: none;
+ pointer-events: none;
+ }
+
+ // IE 8-10 apply "pointer-events" only to SVG elements.
+ // Using an empty SVG instead of an empty text node makes IE
+ // obey the "pointer-events: none" and forwards click events
+ // to the underlying element. The data decodes to:
+ // <svg xmlns="http://www.w3.org/2000/svg"></svg>
+ .ie8 &:before,
+ .ie9 &:before,
+ .ie10 &:before {
+ content: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==);
+ }
+ }
+
+ .#{$primaryStyleName}:focus .#{$primaryStyleName}-cell-focused:before {
+ display: block;
+ }
+
+ .#{$primaryStyleName}.v-disabled:focus .#{$primaryStyleName}-cell-focused:before {
+ // Disabled Grid should not show cell focus outline
+ display: none;
+ }
+
+ // Editor
+
+ .#{$primaryStyleName}-editor {
+ // TODO should be fixed in offset calculations
+ margin-top: -1px;
+ position: absolute;
+ overflow-y: visible;
+ background: $v-grid-editor-background-color;
+ @include box-shadow(0 0 10px 1px rgba(0,0,0,.3));
+
+ > div {
+ position: absolute;
+ @include box-sizing(border-box);
+ border-left: $v-grid-cell-vertical-border;
+
+ &:first-child {
+ border-left: none;
+ }
+
+ .v-textfield,
+ .v-datefield,
+ .v-filterselect {
+ min-width: 100%;
+ max-width: 100%;
+ min-height: 100%;
+ max-height: 100%;
+ border: none;
+ border-radius: 0;
+ }
+
+ .v-textfield-focus,
+ .v-filterselect-focus input {
+ position: relative;
+ z-index: 1;
+ }
+ }
+ }
+
+ .#{$primaryStyleName}-editor-save,
+ .#{$primaryStyleName}-editor-cancel {
+ position: absolute;
+ // TODO remove the inline size from the widgets
+ width: auto !important;
+ height: auto !important;
+ }
+
+ // Renderers
+
+ .#{$primaryStyleName}-cell > .v-progressbar {
+ width: 100%;
+ }
+}
diff --git a/WebContent/VAADIN/themes/chameleon/chameleon.scss b/WebContent/VAADIN/themes/chameleon/chameleon.scss
index 95f81f69c1..b315678308 100644
--- a/WebContent/VAADIN/themes/chameleon/chameleon.scss
+++ b/WebContent/VAADIN/themes/chameleon/chameleon.scss
@@ -1,10 +1,11 @@
+$font-size: 13px !default;
+$line-height: 1.4 !default;
+
@import "../base/base.scss";
@import "common/common.scss";
@import "components/components.scss";
@import "compound/compound.scss";
-$font-size: 13px;
-$line-height: 1.4;
@mixin chameleon {
// TODO move this?
@include base;
diff --git a/WebContent/VAADIN/themes/reindeer-tests/styles.css b/WebContent/VAADIN/themes/reindeer-tests/styles.css
index 679de01b9c..9dd88707d1 100644
--- a/WebContent/VAADIN/themes/reindeer-tests/styles.css
+++ b/WebContent/VAADIN/themes/reindeer-tests/styles.css
@@ -32,3 +32,7 @@
.popup-style .v-datefield-calendarpanel-body {
background: yellow;
}
+
+#escalator .v-escalator-body .v-escalator-cell {
+ height: 50px;
+} \ No newline at end of file
diff --git a/WebContent/VAADIN/themes/reindeer/grid/grid.scss b/WebContent/VAADIN/themes/reindeer/grid/grid.scss
new file mode 100644
index 0000000000..8dacb3ccce
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/grid.scss
@@ -0,0 +1,60 @@
+// Variables defined in reindeer.scss
+
+@mixin reindeer-grid($primaryStyleName: v-grid) {
+
+ .#{$primaryStyleName}-header,
+ .#{$primaryStyleName}-footer {
+ .#{$primaryStyleName}-cell {
+ background-image: url(img/header-bg-light.png);
+ color: #222;
+ font-weight: bold;
+ text-shadow: #f3f5f8 0 1px 0;
+ text-transform: uppercase;
+ }
+ }
+
+ .#{$primaryStyleName}-header-deco,
+ .#{$primaryStyleName}-footer-deco,
+ .#{$primaryStyleName}-horizontal-scrollbar-deco {
+ background-image: url(img/header-bg-light.png);
+ }
+
+ // Selected row
+ .#{$primaryStyleName}-row-selected {
+ color: #fff;
+ text-shadow: #3b5a7a 0 1px 0;
+
+ > .#{$primaryStyleName}-cell {
+ background: #4d749f url(../common/img/sel-bg.png) repeat-x;
+ border-color: #466c90;
+ }
+
+ // Selected and focused
+ > .#{$primaryStyleName}-cell-focused:before {
+ border-color: #b1cde4;
+ }
+ }
+
+ // Sort indicators
+ .#{$primaryStyleName} th.sort-asc,
+ .#{$primaryStyleName} th.sort-desc {
+ padding-right: 16px + $v-grid-cell-padding-horizontal;
+
+ &:after {
+ content: " " attr(sort-order);
+ background: transparent no-repeat right 7px;
+ width: 16px;
+ height: 12px;
+ top: 0;
+ }
+ }
+
+ .#{$primaryStyleName} th.sort-asc:after {
+ background-image: url(img/asc-light.png);
+ }
+
+ .#{$primaryStyleName} th.sort-desc:after {
+ background-image: url(img/desc-light.png);
+ }
+
+}
diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/asc-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/asc-light.png
new file mode 100644
index 0000000000..44ed76001a
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/img/asc-light.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/desc-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/desc-light.png
new file mode 100644
index 0000000000..84d15a0628
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/img/desc-light.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.png
new file mode 100644
index 0000000000..20b34474c7
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/focus-header-bg-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/focus-header-bg-light.png
new file mode 100644
index 0000000000..4e83df03cb
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/img/focus-header-bg-light.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/focus-sel-bg-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/focus-sel-bg-light.png
new file mode 100644
index 0000000000..249fd5917c
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/img/focus-sel-bg-light.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.png
new file mode 100644
index 0000000000..0b913e2ef1
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gif b/WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gif
new file mode 100644
index 0000000000..474b684196
--- /dev/null
+++ b/WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gif
Binary files differ
diff --git a/WebContent/VAADIN/themes/reindeer/progressindicator/progressindicator.scss b/WebContent/VAADIN/themes/reindeer/progressindicator/progressindicator.scss
index 52e4239752..2417202828 100644
--- a/WebContent/VAADIN/themes/reindeer/progressindicator/progressindicator.scss
+++ b/WebContent/VAADIN/themes/reindeer/progressindicator/progressindicator.scss
@@ -11,4 +11,10 @@
background: #f7f9f9 url(img/progress.png);
}
-} \ No newline at end of file
+// Static style
+
+.#{$primaryStyleName}-static .#{$primaryStyleName}-wrapper {
+ background: #dfe2e4 url(img/base-static.gif) repeat-x;
+}
+
+}
diff --git a/WebContent/VAADIN/themes/reindeer/reindeer.scss b/WebContent/VAADIN/themes/reindeer/reindeer.scss
index 485839ecc7..cda571fda0 100644
--- a/WebContent/VAADIN/themes/reindeer/reindeer.scss
+++ b/WebContent/VAADIN/themes/reindeer/reindeer.scss
@@ -1,3 +1,20 @@
+$font-size: 12px !default;
+$line-height: normal !default;
+
+
+// Override Base Grid variables
+$v-grid-border: 1px solid #c2c3c4;
+$v-grid-cell-vertical-border: 1px solid #d4d4d4;
+$v-grid-cell-horizontal-border: none;
+$v-grid-cell-focused-border: 1px solid #0f68ba;
+$v-grid-row-height: 20px;
+$v-grid-row-stripe-background-color: #eff0f1;
+$v-grid-row-selected-background-color: #4d749f;
+$v-grid-header-font-size: 10px;
+$v-grid-header-background-color: rgb(217,219,221);
+$v-grid-cell-padding-horizontal: 6px;
+
+
@import "../base/base.scss";
// common between others for now for backwards compatibility
@@ -12,6 +29,7 @@
@import "datefield/datefield.scss";
@import "inlinedatefield/inlinedatefield.scss";
@import "formlayout/formlayout.scss";
+@import "grid/grid.scss";
@import "label/label.scss";
@import "layouts/layouts.scss";
@import "link/link.scss";
@@ -33,9 +51,6 @@
background: #f5f5f5;
}
-$font-size: 12px;
-$line-height: normal;
-
@mixin reindeer {
@include base;
// TODO @each
@@ -49,6 +64,7 @@ $line-height: normal;
@include reindeer-datefield;
@include reindeer-inlinedatefield;
@include reindeer-formlayout;
+ @include reindeer-grid;
@include reindeer-label;
@include reindeer-layouts;
@include reindeer-link;
@@ -59,7 +75,7 @@ $line-height: normal;
@include reindeer-progressindicator(v-progressbar);
/* For legacy ProgressIndicator component */
@include reindeer-progressindicator(v-progressindicator);
-
+
@include reindeer-select;
@include reindeer-slider;
@include reindeer-splitpanel;
@@ -69,5 +85,3 @@ $line-height: normal;
@include reindeer-tree;
@include reindeer-window;
}
-
-
diff --git a/WebContent/VAADIN/themes/runo/grid/grid.scss b/WebContent/VAADIN/themes/runo/grid/grid.scss
new file mode 100644
index 0000000000..a1081878cc
--- /dev/null
+++ b/WebContent/VAADIN/themes/runo/grid/grid.scss
@@ -0,0 +1,53 @@
+// Variables defined in runo.scss
+
+@mixin runo-grid($primaryStyleName: v-grid) {
+
+ .#{$primaryStyleName}-header,
+ .#{$primaryStyleName}-footer {
+ .#{$primaryStyleName}-cell {
+ background-image: url(img/header-bg.png);
+ color: #393a3c;
+ text-shadow: #fff 0 1px 0;
+ @include box-shadow(inset 1px 0 0 #fff);
+ }
+ }
+
+ .#{$primaryStyleName}-header-deco,
+ .#{$primaryStyleName}-footer-deco,
+ .#{$primaryStyleName}-horizontal-scrollbar-deco {
+ background-image: url(img/header-bg.png);
+ }
+
+ // Selected row
+ .#{$primaryStyleName}-row-selected {
+ color: #fff;
+
+ // Selected and focused
+ > .#{$primaryStyleName}-cell-focused:before {
+ border-color: lighten($v-grid-row-selected-background-color, 20%);
+ }
+ }
+
+ // Sort indicators
+ .#{$primaryStyleName} th.sort-asc,
+ .#{$primaryStyleName} th.sort-desc {
+ padding-right: 30px + $v-grid-cell-padding-horizontal;
+
+ &:after {
+ content: attr(sort-order);
+ background: transparent no-repeat right 50%;
+ width: 30px;
+ height: 36px;
+ top: 0;
+ }
+ }
+
+ .#{$primaryStyleName} th.sort-asc:after {
+ background-image: url(img/sort-asc.png);
+ }
+
+ .#{$primaryStyleName} th.sort-desc:after {
+ background-image: url(img/sort-desc.png);
+ }
+
+}
diff --git a/WebContent/VAADIN/themes/runo/grid/img/header-bg.png b/WebContent/VAADIN/themes/runo/grid/img/header-bg.png
new file mode 100644
index 0000000000..275fbc4382
--- /dev/null
+++ b/WebContent/VAADIN/themes/runo/grid/img/header-bg.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/runo/grid/img/resizer-bg.png b/WebContent/VAADIN/themes/runo/grid/img/resizer-bg.png
new file mode 100644
index 0000000000..d9089775cb
--- /dev/null
+++ b/WebContent/VAADIN/themes/runo/grid/img/resizer-bg.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/runo/grid/img/sort-asc.png b/WebContent/VAADIN/themes/runo/grid/img/sort-asc.png
new file mode 100644
index 0000000000..44e17d5446
--- /dev/null
+++ b/WebContent/VAADIN/themes/runo/grid/img/sort-asc.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/runo/grid/img/sort-desc.png b/WebContent/VAADIN/themes/runo/grid/img/sort-desc.png
new file mode 100644
index 0000000000..35fd0595f8
--- /dev/null
+++ b/WebContent/VAADIN/themes/runo/grid/img/sort-desc.png
Binary files differ
diff --git a/WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gif b/WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gif
new file mode 100644
index 0000000000..474b684196
--- /dev/null
+++ b/WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gif
Binary files differ
diff --git a/WebContent/VAADIN/themes/runo/progressindicator/progressindicator.scss b/WebContent/VAADIN/themes/runo/progressindicator/progressindicator.scss
index 9664a473b2..432123cf1f 100644
--- a/WebContent/VAADIN/themes/runo/progressindicator/progressindicator.scss
+++ b/WebContent/VAADIN/themes/runo/progressindicator/progressindicator.scss
@@ -20,4 +20,10 @@
background: #dfe2e4;
}
-} \ No newline at end of file
+// Static style
+
+.#{$primaryStyleName}-static .#{$primaryStyleName}-wrapper {
+ background: #dfe2e4 url(img/base-static.gif) repeat-x;
+}
+
+}
diff --git a/WebContent/VAADIN/themes/runo/runo.scss b/WebContent/VAADIN/themes/runo/runo.scss
index 33ad35a8af..73566be8c3 100644
--- a/WebContent/VAADIN/themes/runo/runo.scss
+++ b/WebContent/VAADIN/themes/runo/runo.scss
@@ -1,3 +1,22 @@
+$font-size: 13px !default;
+$line-height: 18px !default;
+
+
+// Override Base Grid variables
+$v-grid-border: 1px solid #b6bbbc;
+$v-grid-cell-vertical-border: 1px solid #d4d4d4;
+$v-grid-cell-vertical-border: none;
+$v-grid-cell-horizontal-border: none;
+$v-grid-cell-focused-border: 1px solid #57a7ed;
+$v-grid-row-height: 26px;
+$v-grid-header-row-height: 36px;
+$v-grid-row-background-color: #fff !default;
+$v-grid-row-stripe-background-color:#eff0f1;
+$v-grid-row-selected-background-color: #57a7ed;
+$v-grid-header-font-size: 15px;
+$v-grid-header-background-color: #e7e9ea;
+
+
@import "../base/base.scss";
@import "absolutelayout/absolutelayout.scss";
@@ -9,6 +28,7 @@
@import "datefield/datefield.scss";
@import "inlinedatefield/inlinedatefield.scss";
@import "formlayout/formlayout.scss";
+@import "grid/grid.scss";
@import "gridlayout/gridlayout.scss";
@import "label/label.scss";
@import "link/link.scss";
@@ -32,9 +52,6 @@
background: #e9eced;
}
-$font-size: 13px;
-$line-height: 18px;
-
@mixin runo {
// TODO move?
@include base;
@@ -44,12 +61,13 @@ $line-height: 18px;
@include runo-button;
@include runo-caption;
@include runo-colorpicker;
-
+
@include runo-common;
-
+
@include runo-datefield;
@include runo-inline-datefield;
@include runo-formlayout;
+ @include runo-grid;
@include runo-gridlayout;
@include runo-label;
@include runo-link;
@@ -58,11 +76,11 @@ $line-height: 18px;
@include runo-orderedlayout;
@include runo-panel;
@include runo-popupview;
-
+
@include runo-progressindicator(v-progressbar);
/* For legacy ProgressIndicator component */
@include runo-progressindicator(v-progressindicator);
-
+
@include runo-select;
@include runo-shadow;
@include runo-slider;
diff --git a/WebContent/VAADIN/themes/valo/components/_all.scss b/WebContent/VAADIN/themes/valo/components/_all.scss
index 0efc363a82..52f1d696aa 100644
--- a/WebContent/VAADIN/themes/valo/components/_all.scss
+++ b/WebContent/VAADIN/themes/valo/components/_all.scss
@@ -105,7 +105,7 @@
}
@if v-is-included(grid) {
- @include valo-grid(v-escalator);
+ @include valo-grid;
}
@if v-is-included(textfield) {
diff --git a/WebContent/VAADIN/themes/valo/components/_escalator.scss b/WebContent/VAADIN/themes/valo/components/_escalator.scss
deleted file mode 100644
index 06ce2e6142..0000000000
--- a/WebContent/VAADIN/themes/valo/components/_escalator.scss
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- *
- *
- * @param {string} $primaryStyleName (v-escalator) -
- *
- * @group escalator
- */
-@mixin valo-escalator($primaryStyleName : v-escalator) {
-
-$background-color: white;
-$border-color: #aaa;
-
-.#{$primaryStyleName} {
- position: relative;
- background-color: $background-color;
-}
-
-.#{$primaryStyleName}-scroller {
- position: absolute;
- overflow: auto;
- z-index: 20;
-}
-
-.#{$primaryStyleName}-scroller-horizontal {
- left: 0; /* Left position adjusted to align with frozen columns */
- right: 0;
- bottom: 0;
- overflow-y: hidden;
- -ms-overflow-y: hidden;
-}
-
-.#{$primaryStyleName}-scroller-vertical {
- right: 0;
- top: 0; /* this will be overridden by code, but it's a good default behavior */
- bottom: 0; /* this will be overridden by code, but it's a good default behavior */
- overflow-x: hidden;
- -ms-overflow-x: hidden;
-}
-
-.#{$primaryStyleName}-tablewrapper {
- position: absolute;
- overflow: hidden;
-}
-
-.#{$primaryStyleName}-tablewrapper > table {
- border-spacing: 0;
- table-layout: fixed;
- width: inherit; /* a decent default fallback */
-}
-
-.#{$primaryStyleName}-header,
-.#{$primaryStyleName}-body,
-.#{$primaryStyleName}-footer {
- position: absolute;
- left: 0;
- width: inherit;
- z-index: 10;
-}
-
-.#{$primaryStyleName}-header { top: 0; }
-.#{$primaryStyleName}-footer { bottom: 0; }
-
-.#{$primaryStyleName}-body {
- z-index: 0;
- top: 0;
-
- .#{$primaryStyleName}-row {
- position: absolute;
- top: 0;
- left: 0;
- }
-}
-
-.#{$primaryStyleName}-row {
- display: block;
-
- .v-ie8 & {
- /* IE8 doesn't let table rows be longer than body only with display block. Moar hax. */
- float: left;
- clear: left;
-
- /*
- * The inline style of margin-top from the <tbody> to offset the header's dimension is,
- * for some strange reason, inherited into each contained <tr>.
- * We need to cancel it:
- */
- margin-top: 0;
- }
-
- > td, > th {
- /* IE8 likes the bgcolor here instead of on the row */
- background-color: $background-color;
- }
-}
-
-
-.#{$primaryStyleName}-row {
- width: inherit;
-}
-
-.#{$primaryStyleName}-cell {
- display: block;
- float: left;
- border: 1px solid $border-color;
- padding: 2px;
- white-space: nowrap;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-.#{$primaryStyleName}-cell.frozen {
- position: relative;
- z-index: 0;
-}
-
-} \ No newline at end of file
diff --git a/WebContent/VAADIN/themes/valo/components/_grid.scss b/WebContent/VAADIN/themes/valo/components/_grid.scss
index cf06167337..2e76434709 100644
--- a/WebContent/VAADIN/themes/valo/components/_grid.scss
+++ b/WebContent/VAADIN/themes/valo/components/_grid.scss
@@ -1,12 +1,98 @@
-@import "escalator";
+@import "table";
+
+$v-grid-row-background-color: valo-table-background-color() !default;
+$v-grid-row-stripe-background-color: scale-color($v-grid-row-background-color, $lightness: if(color-luminance($v-grid-row-background-color) < 10, 4%, -4%)) !default;
+
+$v-grid-border: valo-border($color: $v-grid-row-background-color, $strength: 0.8) !default;
+$v-grid-cell-focused-border: max(2px, first-number($v-border)) solid $v-selection-color !default;
+
+$v-grid-row-height: $v-table-row-height !default;
+$v-grid-row-selected-background-color: $v-selection-color !default;
+
+$v-grid-header-font-size: $v-table-header-font-size !default;
+$v-grid-header-background-color: $v-background-color !default;
+
+$v-grid-cell-padding-horizontal: $v-table-cell-padding-horizontal !default;
+
+
+@import "../../base/grid/grid";
/**
*
*
- * @param {string} $primary-styleName (v-grid) -
+ * @param {string} $primary-stylename (v-grid) -
*
* @group grid
*/
-@mixin valo-grid($primary-styleName : v-grid) {
- @include valo-escalator($primary-styleName);
-} \ No newline at end of file
+@mixin valo-grid ($primary-stylename: v-grid) {
+
+ @include base-grid($primary-stylename);
+
+ .#{$primary-stylename} {
+ @include user-select(text);
+ background-color: $v-background-color;
+ }
+
+ .#{$primary-stylename}-header .#{$primary-stylename}-cell {
+ @include valo-gradient($v-grid-header-background-color);
+ text-shadow: valo-text-shadow($font-color: valo-font-color($v-grid-header-background-color), $background-color: $v-grid-header-background-color);
+ }
+
+ .#{$primary-stylename}-footer .#{$primary-stylename}-cell {
+ @include valo-gradient($v-grid-footer-background-color);
+ text-shadow: valo-text-shadow($font-color: valo-font-color($v-grid-footer-background-color), $background-color: $v-grid-footer-background-color);
+ }
+
+ .#{$primary-stylename}-header-deco {
+ @include valo-gradient($v-grid-header-background-color);
+ }
+
+ .#{$primary-stylename}-footer-deco,
+ .#{$primary-stylename}-horizontal-scrollbar-deco {
+ @include valo-gradient($v-grid-footer-background-color);
+ }
+
+ // Selected
+ .#{$primary-stylename}-row-selected {
+ > .#{$primary-stylename}-cell {
+ @include valo-gradient($v-selection-color);
+ color: valo-font-color($v-selection-color);
+ text-shadow: valo-text-shadow($font-color: valo-font-color($v-selection-color), $background-color: $v-selection-color);
+ border-color: adjust-color($v-selection-color, $lightness: -8%, $saturation: -8%);
+ }
+
+ > .#{$primary-stylename}-cell-focused:before {
+ border-color: adjust-color($v-selection-color, $lightness: 20%);
+ }
+ }
+
+ .#{$primary-stylename}-editor-save,
+ .#{$primary-stylename}-editor-cancel {
+ @include valo-button-static-style;
+ @include valo-button-style($unit-size: $v-unit-size--small, $font-size: $v-font-size--small);
+ }
+
+ // Customize scrollbars
+ .#{$primary-stylename}-scroller {
+ &::-webkit-scrollbar {
+ border: none;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ border: 4px solid transparent;
+ background: if(is-dark-color($v-grid-header-background-color), rgba(255,255,255,.3), rgba(0,0,0,.3));
+ -webkit-background-clip: content-box;
+ background-clip: content-box;
+ }
+ }
+
+ .#{$primary-stylename}-scroller-vertical::-webkit-scrollbar-thumb {
+ min-height: 30px;
+ }
+
+ .#{$primary-stylename}-scroller-horizontal::-webkit-scrollbar-thumb {
+ min-width: 30px;
+ }
+
+}
diff --git a/all/build.xml b/all/build.xml
index 65980e9b05..37f728e529 100644
--- a/all/build.xml
+++ b/all/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-all" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant" xmlns:antcontrib="antlib:net.sf.antcontrib">
+<project name="vaadin-all" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant" xmlns:antcontrib="antlib:net.sf.antcontrib">
<description>
Compiles a zip containing all jars + dependencies
</description>
@@ -10,11 +11,13 @@
<!-- global properties -->
<property name="module.name" value="vaadin-all" />
<property name="result.dir" value="result" />
- <property name="javadoc.jar" location="${result.dir}/lib/vaadin-all-${vaadin.version}-javadoc.jar" />
+ <property name="javadoc.jar"
+ location="${result.dir}/lib/vaadin-all-${vaadin.version}-javadoc.jar" />
<property name="temp.dir" location="${result.dir}/temp" />
<property name="temp.deps.dir" value="${temp.dir}/lib" />
<property name="javadoc.temp.dir" location="${result.dir}/javadoc-temp" />
- <property name="zip.file" location="${result.dir}/lib/${module.name}-${vaadin.version}.zip" />
+ <property name="zip.file"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.zip" />
<path id="classpath.javadoc">
<fileset dir="${temp.deps.dir}" includes="*.jar">
@@ -23,61 +26,70 @@
<target name="fetch.module.and.dependencies">
<fail unless="module" message="No 'module' parameter given" />
- <ivy:cachepath pathid="module.and.deps" inline="true" organisation="com.vaadin" module="vaadin-${module}" revision="${vaadin.version}" />
+ <ivy:cachepath pathid="module.and.deps" inline="true"
+ organisation="com.vaadin" module="vaadin-${module}"
+ revision="${vaadin.version}" />
<copy todir="${temp.dir}" flatten="true">
<path refid="module.and.deps" />
</copy>
</target>
<target name="unzip.to.javadoctemp">
- <property name="file" location="${temp.dir}/vaadin-${module}-${vaadin.version}.jar" />
+ <property name="file"
+ location="${temp.dir}/vaadin-${module}-${vaadin.version}.jar" />
<unzip src="${file}" dest="${javadoc.temp.dir}" />
</target>
<target name="javadoc" depends="copy-jars">
- <!-- Ensure filtered webcontent files are available -->
- <antcall target="common.filter.webcontent" />
-
- <antcontrib:if>
- <isset property="nojavadoc" />
- <then>
- <jar file="${javadoc.jar}" compress="true">
- <fileset refid="common.files.for.all.jars" />
- </jar>
- </then>
- <else>
- <!-- Unpack all source files to javadoc.temp.dir -->
- <antcontrib:foreach list="${modules.to.publish.to.maven}" target="unzip.to.javadoctemp" param="module" />
-
- <property name="javadoc.dir" location="${result.dir}/javadoc" />
- <property name="title" value="Vaadin ${vaadin.version} API" />
- <javadoc maxmemory="1024m" destdir="${javadoc.dir}" author="true" version="true"
- use="true" windowtitle="${title}" encoding="utf-8" stylesheetfile="javadoc.css">
- <packageset dir="${javadoc.temp.dir}">
- <!-- TODO Javadoc throws ClassCastException if this is included
- (#9660) -->
- <exclude name="com/google/gwt/uibinder/elementparsers" />
- </packageset>
- <doctitle>&lt;h1>${title}&lt;/h1></doctitle>
- <!-- <header><![CDATA[<script type="text/javascript" src=".html-style/style.js"></script>]]></header> -->
- <bottom>${javadoc.bottom}</bottom>
- <link offline="true" href="http://docs.oracle.com/javase/6/docs/api/" packagelistLoc="build/javadoc/j2se-1.6.0" />
- <link offline="true" href="http://java.sun.com/j2ee/1.4/docs/api/" packagelistLoc="build/javadoc/j2ee-1.4" />
- <classpath refid="classpath.javadoc" />
- </javadoc>
-
- <!-- Create a javadoc jar -->
- <jar file="${javadoc.jar}" compress="true">
- <fileset dir="${javadoc.dir}" />
- <fileset refid="common.files.for.all.jars" />
- </jar>
- </else>
- </antcontrib:if>
+ <antcontrib:if>
+ <isset property="nojavadoc" />
+ <then>
+ <jar file="${javadoc.jar}" compress="true">
+ <fileset dir="${common.jarfiles.dir}" />
+ </jar>
+ </then>
+ <else>
+ <!-- Unpack all source files to javadoc.temp.dir -->
+ <antcontrib:foreach list="${modules.to.publish.to.maven}"
+ target="unzip.to.javadoctemp" param="module" />
+
+ <property name="javadoc.dir" location="${result.dir}/javadoc" />
+ <property name="title" value="Vaadin ${vaadin.version} API" />
+ <javadoc maxmemory="1024m" destdir="${javadoc.dir}"
+ author="true" version="true" use="true" windowtitle="${title}"
+ encoding="utf-8" stylesheetfile="javadoc.css">
+ <packageset dir="${javadoc.temp.dir}">
+ <!-- TODO Javadoc throws ClassCastException if this
+ is included (#9660) -->
+ <exclude
+ name="com/google/gwt/uibinder/elementparsers" />
+ </packageset>
+ <doctitle>&lt;h1>${title}&lt;/h1></doctitle>
+ <!-- <header><![CDATA[<script type="text/javascript"
+ src=".html-style/style.js"></script>]]></header> -->
+ <bottom>${javadoc.bottom}</bottom>
+ <link offline="true"
+ href="http://docs.oracle.com/javase/6/docs/api/"
+ packagelistLoc="build/javadoc/j2se-1.6.0" />
+ <link offline="true"
+ href="http://java.sun.com/j2ee/1.4/docs/api/"
+ packagelistLoc="build/javadoc/j2ee-1.4" />
+ <classpath refid="classpath.javadoc" />
+ </javadoc>
+
+ <!-- Create a javadoc jar -->
+ <jar file="${javadoc.jar}" compress="true">
+ <fileset dir="${javadoc.dir}" />
+ <fileset dir="${common.jarfiles.dir}" />
+ </jar>
+ </else>
+ </antcontrib:if>
</target>
<target name="copy-jars">
<delete dir="${temp.dir}" />
- <antcontrib:foreach list="${modules.to.publish.to.maven}" target="fetch.module.and.dependencies" param="module" />
+ <antcontrib:foreach list="${modules.to.publish.to.maven}"
+ target="fetch.module.and.dependencies" param="module" />
<!-- All jars are now in temp.dir. Still need to separate vaadin
and deps -->
<move todir="${temp.deps.dir}">
@@ -90,9 +102,6 @@
</target>
<target name="zip" depends="copy-jars, javadoc">
- <!-- Ensure filtered webcontent files are available -->
- <antcall target="common.filter.webcontent" />
-
<zip destfile="${zip.file}">
<fileset dir="${temp.dir}">
<!-- Avoid conflicts with servlet and portlet API. They are
@@ -106,9 +115,8 @@
<exclude name="*.pom" />
<exclude name="*-javadoc.jar" />
<exclude name="*-sources.jar" />
-
</fileset>
- <fileset refid="common.files.for.all.jars" />
+ <fileset dir="${common.jarfiles.dir}" />
<fileset dir="${result.dir}/..">
<include name="README.TXT" />
</fileset>
diff --git a/all/ivy.xml b/all/ivy.xml
index 81768555fa..156588485f 100644
--- a/all/ivy.xml
+++ b/all/ivy.xml
@@ -31,6 +31,7 @@
<dependency org="com.vaadin" name="vaadin-client-compiled"
rev="${vaadin.version}" />
<dependency org="com.vaadin" name="vaadin-push" rev="${vaadin.version}" />
+ <dependency org="com.vaadin" name="vaadin-widgets" rev="${vaadin.version}" />
</dependencies>
diff --git a/buildhelpers/build.xml b/buildhelpers/build.xml
index 49c290e9f1..05b8c8397f 100644
--- a/buildhelpers/build.xml
+++ b/buildhelpers/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-buildhelpers" basedir="." default="publish-local">
+<project name="vaadin-buildhelpers" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
Compiles build helpers used when building other
modules.
@@ -13,17 +14,26 @@
<property name="result.dir" location="result" />
<path id="classpath.compile.custom" />
- <target name="jar">
- <antcall target="common.jar">
- <reference torefid="extra.jar.includes" refid="empty.reference" />
- </antcall>
+ <property name="filtered.webcontent.dir" location="${result.dir}/WebContent" />
+ <property name="release-notes-tickets-file" location="${result.dir}/release-notes-tickets.html" />
+ <property name="release-notes-authors-file" location="${result.dir}/release-notes-authors.html" />
+
+ <target name="jar" depends="filter.webcontent">
+ <antcall target="common.compile" />
+ <property name="result.jar"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" />
+ <property name="classes" location="${result.dir}/classes" />
+ <property name="src" location="${result.dir}/../src" />
+
+ <jar destfile="${result.jar}" duplicate="fail" index="true">
+ <fileset dir="${classes}" excludes="${classes.exclude}"
+ erroronmissingdir="false" />
+ <fileset dir="${filtered.webcontent.dir}/.."
+ includes="WebContent/**" />
+ </jar>
</target>
<target name="publish-local" depends="jar">
- <antcall target="common.sources.jar">
- <reference torefid="extra.jar.includes" refid="empty.reference" />
- </antcall>
- <antcall target="common.javadoc.jar" />
<antcall target="common.publish-local" />
</target>
@@ -31,28 +41,96 @@
<antcall target="common.clean" />
</target>
- <target name="checkstyle">
- <antcall target="common.checkstyle">
- <param name="cs.src" location="src" />
+ <target name="filter.webcontent"
+ depends="fetch-release-notes-tickets,fetch-release-notes-authors">
+ <!-- Running without build.release-notes will cause an error, which
+ is ignored -->
+ <loadfile property="release-notes-tickets" srcFile="${release-notes-tickets-file}"
+ failonerror="false" />
+ <loadfile property="release-notes-authors" srcFile="${release-notes-authors-file}"
+ failonerror="false" />
+
+ <delete dir="${filtered.webcontent.dir}" />
+ <copy todir="${filtered.webcontent.dir}">
+ <fileset dir="${vaadin.basedir}/WebContent">
+ <include name="img/**" />
+ </fileset>
+ </copy>
+ <copy todir="${filtered.webcontent.dir}">
+ <fileset dir="${vaadin.basedir}/WebContent">
+ <patternset>
+ <include name="release-notes.html" />
+ <include name="license.html" />
+ <include name="licenses/**" />
+ <include name="css/**" />
+ </patternset>
+ </fileset>
+ <filterchain>
+ <expandproperties />
+ <replacetokens begintoken="@" endtoken="@">
+ <token key="version" value="${vaadin.version}" />
+ </replacetokens>
+ <replacetokens begintoken="@" endtoken="@">
+ <token key="version-minor"
+ value="${vaadin.version.major}.${vaadin.version.minor}" />
+ </replacetokens>
+ <replacetokens begintoken="@" endtoken="@">
+ <token key="builddate" value="${build.date}" />
+ </replacetokens>
+ <replacetokens begintoken="@" endtoken="@">
+ <token key="release-notes-tickets" value="${release-notes-tickets}" />
+ </replacetokens>
+ <replacetokens begintoken="@" endtoken="@">
+ <token key="release-notes-authors" value="${release-notes-authors}" />
+ </replacetokens>
+ </filterchain>
+ </copy>
+ </target>
+
+
+ <target name="fetch-release-notes-tickets" if="build.release-notes">
+ <mkdir dir="${filtered.webcontent.dir}" />
+ <antcall target="exec-buildhelper">
+ <param name="main.class"
+ value="com.vaadin.buildhelpers.FetchReleaseNotesTickets" />
+ <param name="output" location="${release-notes-tickets-file}" />
</antcall>
</target>
- <target name="fetch-release-notes-tickets">
- <antcall target="common.exec-buildhelper">
- <param name="main.class" value="com.vaadin.buildhelpers.FetchReleaseNotesTickets" />
- <param name="output" value="${output}" />
- <param name="src" value="src" />
+ <target name="fetch-release-notes-authors" if="build.release-notes">
+ <copy file="src/com/vaadin/buildhelpers/authormap.properties"
+ tofile="${result.dir}/classes/com/vaadin/buildhelpers/authormap.properties" />
+
+ <mkdir dir="${filtered.webcontent.dir}" />
+ <antcall target="exec-buildhelper">
+ <param name="main.class"
+ value="com.vaadin.buildhelpers.FetchReleaseNotesAuthors" />
+ <param name="output" location="${release-notes-authors-file}" />
</antcall>
+ <delete
+ file="${result.dir}/classes/com/vaadin/buildhelpers/authormap.properties" />
+ </target>
+
+ <target name="exec-buildhelper">
+ <antcall target="common.compile" />
+ <fail unless="main.class" message="No main class given in 'main.class'" />
+ <fail unless="output" message="No output file given in 'output'" />
+ <ivy:resolve log="download-only" conf="build" />
+ <ivy:cachepath pathid="deps" />
+ <java classname="${main.class}" output="${output}"
+ failonerror="true" fork="yes">
+ <classpath>
+ <pathelement location="${result.dir}/classes" />
+ </classpath>
+ <classpath refid="deps" />
+ <jvmarg value="-Dvaadin.version=${vaadin.version}" />
+ </java>
</target>
- <target name="fetch-release-notes-authors">
- <copy file="src/com/vaadin/buildhelpers/authormap.properties" tofile="result/classes/com/vaadin/buildhelpers/authormap.properties" />
- <antcall target="common.exec-buildhelper">
- <param name="main.class" value="com.vaadin.buildhelpers.FetchReleaseNotesAuthors" />
- <param name="output" value="${output}" />
- <param name="src" value="src" />
+ <target name="checkstyle">
+ <antcall target="common.checkstyle">
+ <param name="cs.src" location="src" />
</antcall>
- <delete file="result/classes/com/vaadin/buildhelpers/authormap.properties" />
</target>
<target name="test" depends="checkstyle">
diff --git a/buildhelpers/ivy.xml b/buildhelpers/ivy.xml
index cf04bfdc5d..8053328b54 100644
--- a/buildhelpers/ivy.xml
+++ b/buildhelpers/ivy.xml
@@ -21,10 +21,6 @@
</configurations>
<publications>
<artifact type="jar" />
- <artifact type="source" ext="jar" m:classifier="sources" />
- <artifact type="javadoc" ext="jar" m:classifier="javadoc" />
- <artifact type="pom" ext="pom" />
-
</publications>
<dependencies>
<!-- client-compiler, server and uitest also use commons-io -->
diff --git a/client-compiled/build.xml b/client-compiled/build.xml
index 78757f5ceb..fb4f26bc73 100644
--- a/client-compiled/build.xml
+++ b/client-compiled/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-client-compiled" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-client-compiled" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
Compiled (JS+HTML) version of client side
</description>
@@ -16,8 +17,10 @@
<property name="gwtar.dir" location="${result.dir}/gwtar" />
<property name="work.dir" location="${result.dir}/work" />
<property name="module.output.dir" location="${result.dir}/VAADIN/widgetsets" />
- <property name="compiled.jar" location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" />
- <property name="compiled-cache.jar" location="${result.dir}/lib/${module.name}-cache-${vaadin.version}.jar" />
+ <property name="compiled.jar"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" />
+ <property name="compiled-cache.jar"
+ location="${result.dir}/lib/${module.name}-cache-${vaadin.version}.jar" />
<union id="jar.includes">
<fileset dir="${result.dir}">
@@ -39,13 +42,18 @@
<target name="compile-module-cache">
- <fail unless="module" message="You must give the module to compile in the 'module' parameter" />
- <ivy:resolve log="download-only" resolveid="common" conf="compile-module" />
- <ivy:cachepath pathid="classpath.compile.widgetset" conf="compile-module" />
+ <fail unless="module"
+ message="You must give the module to compile in the 'module' parameter" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="compile-module" />
+ <ivy:cachepath pathid="classpath.compile.widgetset"
+ conf="compile-module" />
<echo>Creating gwtar files for ${module} in ${gwtar.dir}</echo>
<!-- Produce gwtar files for the separate JAR -->
- <java classname="com.google.gwt.dev.CompileModule" classpathref="classpath.compile.widgetset" failonerror="yes" fork="yes" maxmemory="512m">
+ <java classname="com.google.gwt.dev.CompileModule"
+ classpathref="classpath.compile.widgetset" failonerror="yes"
+ fork="yes" maxmemory="512m">
<arg value="-out" />
<arg value="${gwtar.dir}" />
<arg value="-strict" />
@@ -58,20 +66,24 @@
</target>
<target name="compile-module">
- <fail unless="module" message="You must give the module to compile in the 'module' parameter" />
+ <fail unless="module"
+ message="You must give the module to compile in the 'module' parameter" />
<property name="style" value="OBF" />
<property name="localWorkers" value="6" />
<property name="extraParams" value="" />
- <ivy:resolve log="download-only" resolveid="common" conf="compile-module" />
- <ivy:cachepath pathid="classpath.compile.widgetset" conf="compile-module" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="compile-module" />
+ <ivy:cachepath pathid="classpath.compile.widgetset"
+ conf="compile-module" />
<mkdir dir="${module.output.dir}" />
<echo>Compiling ${module} to ${module.output.dir}</echo>
<!-- compile the module -->
- <java classname="com.google.gwt.dev.Compiler" classpathref="classpath.compile.widgetset" failonerror="yes" fork="yes" maxmemory="512m">
+ <java classname="com.google.gwt.dev.Compiler" classpathref="classpath.compile.widgetset"
+ failonerror="yes" fork="yes" maxmemory="512m">
<classpath location="${compiled-cache.jar}" />
<arg value="-workDir" />
<arg value="${work.dir}" />
@@ -86,7 +98,7 @@
<arg value="${localWorkers}" />
<arg value="-strict" />
<!-- Disabled for now as it breaks code, e.g. ButtonWithShortcutNotRendered -->
- <!-- <arg value="-XenableClosureCompiler" />-->
+ <!-- <arg value="-XenableClosureCompiler" /> -->
<arg line="${extraParams}" />
<arg value="${module}" />
@@ -100,15 +112,12 @@
</target>
<target name="client-compiled-cache.jar" depends="default-widgetset-cache">
- <!-- Ensure filtered webcontent files are available -->
- <antcall target="common.filter.webcontent" />
-
<jar file="${compiled-cache.jar}" compress="true">
<fileset dir="${gwtar.dir}">
<include name="**/*.gwtar" />
</fileset>
<union refid="client-compiled-cache.gwt.includes" />
- <fileset refid="common.files.for.all.jars" />
+ <fileset dir="${common.jarfiles.dir}" />
</jar>
</target>
diff --git a/client-compiler/build.xml b/client-compiler/build.xml
index be8dec18bc..5a5d1a6161 100644
--- a/client-compiler/build.xml
+++ b/client-compiler/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-client-compiler" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-client-compiler" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
Compiles build helpers used when building other
modules.
@@ -22,9 +23,8 @@
</fileset>
</path>
<property name="extra.classes" value="**/*.properties" />
- <!-- don't try to copy the same files twice (first from classes and then
- from sources) in order for the build not to fail when packaging the
- JAR -->
+ <!-- don't try to copy the same files twice (first from classes and then
+ from sources) in order for the build not to fail when packaging the JAR -->
<property name="jar.exclude" value="**/*.properties" />
<union id="compiler.includes">
<union refid="client-compiler.gwt.includes" />
@@ -35,7 +35,8 @@
<target name="jar">
<!-- Get Git revision -->
- <exec executable="git" outputproperty="git.revision" failifexecutionfails="false" errorproperty="">
+ <exec executable="git" outputproperty="git.revision"
+ failifexecutionfails="false" errorproperty="">
<arg value="describe" />
<arg value="--tags" />
<arg value="--always" />
@@ -72,7 +73,7 @@
<target name="test" depends="checkstyle">
<antcall target="common.test.run" />
- <!--<echo>WHAT? No tests for ${module.name}!</echo>-->
+ <!--<echo>WHAT? No tests for ${module.name}!</echo> -->
</target>
</project>
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java
index a6ca690a8a..884852f7c8 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java
@@ -47,6 +47,7 @@ import com.google.gwt.user.rebind.SourceWriter;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.annotations.OnStateChange;
+import com.vaadin.client.communication.JsonDecoder;
import com.vaadin.client.metadata.ConnectorBundleLoader;
import com.vaadin.client.metadata.ConnectorBundleLoader.CValUiInfo;
import com.vaadin.client.metadata.InvokationHandler;
@@ -54,6 +55,7 @@ import com.vaadin.client.metadata.OnStateChangeMethod;
import com.vaadin.client.metadata.ProxyHandler;
import com.vaadin.client.metadata.TypeData;
import com.vaadin.client.metadata.TypeDataStore;
+import com.vaadin.client.metadata.TypeDataStore.MethodAttribute;
import com.vaadin.client.ui.UnknownComponentConnector;
import com.vaadin.server.widgetsetutils.metadata.ClientRpcVisitor;
import com.vaadin.server.widgetsetutils.metadata.ConnectorBundle;
@@ -61,12 +63,13 @@ import com.vaadin.server.widgetsetutils.metadata.ConnectorInitVisitor;
import com.vaadin.server.widgetsetutils.metadata.GeneratedSerializer;
import com.vaadin.server.widgetsetutils.metadata.OnStateChangeVisitor;
import com.vaadin.server.widgetsetutils.metadata.Property;
+import com.vaadin.server.widgetsetutils.metadata.RendererVisitor;
import com.vaadin.server.widgetsetutils.metadata.ServerRpcVisitor;
import com.vaadin.server.widgetsetutils.metadata.StateInitVisitor;
import com.vaadin.server.widgetsetutils.metadata.TypeVisitor;
import com.vaadin.server.widgetsetutils.metadata.WidgetInitVisitor;
-import com.vaadin.shared.annotations.Delayed;
import com.vaadin.shared.annotations.DelegateToWidget;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.ui.Connect;
@@ -453,6 +456,10 @@ public class ConnectorBundleLoaderFactory extends Generator {
writer.println("var data = {");
writer.indent();
+ if (property.getAnnotation(NoLayout.class) != null) {
+ writer.println("noLayout: 1, ");
+ }
+
writer.println("setter: function(bean, value) {");
writer.indent();
property.writeSetterBody(logger, writer, "bean", "value");
@@ -495,7 +502,7 @@ public class ConnectorBundleLoaderFactory extends Generator {
writeInvokers(logger, w, bundle);
writeParamTypes(w, bundle);
writeProxys(w, bundle);
- writeDelayedInfo(w, bundle);
+ writeMethodAttributes(logger, w, bundle);
w.println("%s(store);", loadNativeJsMethodName);
@@ -503,6 +510,7 @@ public class ConnectorBundleLoaderFactory extends Generator {
// this after the JS property data has been initialized
writePropertyTypes(logger, w, bundle);
writeSerializers(logger, w, bundle);
+ writePresentationTypes(w, bundle);
writeDelegateToWidget(logger, w, bundle);
writeOnStateChangeHandlers(logger, w, bundle);
}
@@ -684,6 +692,21 @@ public class ConnectorBundleLoaderFactory extends Generator {
}
}
+ private void writePresentationTypes(SplittingSourceWriter w,
+ ConnectorBundle bundle) {
+ Map<JClassType, JType> presentationTypes = bundle
+ .getPresentationTypes();
+ for (Entry<JClassType, JType> entry : presentationTypes.entrySet()) {
+
+ w.print("store.setPresentationType(");
+ writeClassLiteral(w, entry.getKey());
+ w.print(", ");
+ writeClassLiteral(w, entry.getValue());
+ w.println(");");
+ w.splitIfNeeded();
+ }
+ }
+
private void writePropertyTypes(TreeLogger logger, SplittingSourceWriter w,
ConnectorBundle bundle) {
Set<Property> properties = bundle.getNeedsProperty();
@@ -700,32 +723,20 @@ public class ConnectorBundleLoaderFactory extends Generator {
}
}
- private void writeDelayedInfo(SplittingSourceWriter w,
- ConnectorBundle bundle) {
- Map<JClassType, Set<JMethod>> needsDelayedInfo = bundle
- .getNeedsDelayedInfo();
- Set<Entry<JClassType, Set<JMethod>>> entrySet = needsDelayedInfo
- .entrySet();
- for (Entry<JClassType, Set<JMethod>> entry : entrySet) {
- JClassType type = entry.getKey();
- Set<JMethod> methods = entry.getValue();
- for (JMethod method : methods) {
- Delayed annotation = method.getAnnotation(Delayed.class);
- if (annotation != null) {
- w.print("store.setDelayed(");
- writeClassLiteral(w, type);
- w.print(", \"");
- w.print(escape(method.getName()));
- w.println("\");");
-
- if (annotation.lastOnly()) {
- w.print("store.setLastOnly(");
- writeClassLiteral(w, type);
- w.print(", \"");
- w.print(escape(method.getName()));
- w.println("\");");
- }
-
+ private void writeMethodAttributes(TreeLogger logger,
+ SplittingSourceWriter w, ConnectorBundle bundle) {
+ for (Entry<JClassType, Map<JMethod, Set<MethodAttribute>>> typeEntry : bundle
+ .getMethodAttributes().entrySet()) {
+ JClassType type = typeEntry.getKey();
+ for (Entry<JMethod, Set<MethodAttribute>> methodEntry : typeEntry
+ .getValue().entrySet()) {
+ JMethod method = methodEntry.getKey();
+ Set<MethodAttribute> attributes = methodEntry.getValue();
+ for (MethodAttribute attribute : attributes) {
+ w.println("store.setMethodAttribute(%s, \"%s\", %s.%s);",
+ getClassLiteralString(type), method.getName(),
+ MethodAttribute.class.getCanonicalName(),
+ attribute.name());
w.splitIfNeeded();
}
}
@@ -972,7 +983,16 @@ public class ConnectorBundleLoaderFactory extends Generator {
w.print(", ");
}
String parameterTypeName = getBoxedTypeName(parameterType);
- w.print("(" + parameterTypeName + ") params[" + i + "]");
+
+ if (parameterTypeName.startsWith("elemental.json.Json")) {
+ // Need to pass through native method to allow casting Object to
+ // JSO if the value is a string
+ w.print("%s.<%s>obj2jso(params[%d])",
+ JsonDecoder.class.getCanonicalName(),
+ parameterTypeName, i);
+ } else {
+ w.print("(" + parameterTypeName + ") params[" + i + "]");
+ }
}
w.println(");");
@@ -1240,8 +1260,9 @@ public class ConnectorBundleLoaderFactory extends Generator {
throws NotFoundException {
List<TypeVisitor> visitors = Arrays.<TypeVisitor> asList(
new ConnectorInitVisitor(), new StateInitVisitor(),
- new WidgetInitVisitor(), new ClientRpcVisitor(),
- new ServerRpcVisitor(), new OnStateChangeVisitor());
+ new WidgetInitVisitor(), new RendererVisitor(),
+ new ClientRpcVisitor(), new ServerRpcVisitor(),
+ new OnStateChangeVisitor());
for (TypeVisitor typeVisitor : visitors) {
typeVisitor.init(oracle);
}
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java
index 6ffd6c5462..0049ae9b50 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ArraySerializer.java
@@ -19,12 +19,14 @@ package com.vaadin.server.widgetsetutils.metadata;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JType;
-import com.google.gwt.json.client.JSONArray;
import com.google.gwt.user.rebind.SourceWriter;
import com.vaadin.client.communication.JsonDecoder;
import com.vaadin.client.communication.JsonEncoder;
import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory;
+import elemental.json.Json;
+import elemental.json.JsonArray;
+
public class ArraySerializer extends JsonSerializer {
private final JArrayType arrayType;
@@ -40,12 +42,12 @@ public class ArraySerializer extends JsonSerializer {
JType leafType = arrayType.getLeafType();
int rank = arrayType.getRank();
- w.println(JSONArray.class.getName() + " jsonArray = " + jsonValue
- + ".isArray();");
+ w.println(JsonArray.class.getName() + " jsonArray = ("
+ + JsonArray.class.getName() + ")" + jsonValue + ";");
// Type value = new Type[jsonArray.size()][][];
w.print(arrayType.getQualifiedSourceName() + " value = new "
- + leafType.getQualifiedSourceName() + "[jsonArray.size()]");
+ + leafType.getQualifiedSourceName() + "[jsonArray.length()]");
for (int i = 1; i < rank; i++) {
w.print("[]");
}
@@ -75,8 +77,8 @@ public class ArraySerializer extends JsonSerializer {
String value, String applicationConnection) {
JType componentType = arrayType.getComponentType();
- w.println(JSONArray.class.getName() + " values = new "
- + JSONArray.class.getName() + "();");
+ w.println(JsonArray.class.getName() + " values = "
+ + Json.class.getName() + ".createArray();");
// JPrimitiveType primitive = componentType.isPrimitive();
w.println("for (int i = 0; i < " + value + ".length; i++) {");
w.indent();
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java
index 856f67657f..992a012005 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ClientRpcVisitor.java
@@ -24,6 +24,8 @@ 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.vaadin.client.metadata.TypeDataStore.MethodAttribute;
+import com.vaadin.shared.annotations.NoLayout;
public class ClientRpcVisitor extends TypeVisitor {
@Override
@@ -39,6 +41,10 @@ public class ClientRpcVisitor extends TypeVisitor {
bundle.setNeedsInvoker(type, method);
bundle.setNeedsParamTypes(type, method);
+ if (method.getAnnotation(NoLayout.class) != null) {
+ bundle.setMethodAttribute(type, method,
+ MethodAttribute.NO_LAYOUT);
+ }
JType[] parameterTypes = method.getParameterTypes();
for (JType paramType : parameterTypes) {
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java
index e8a384298f..405925a920 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java
@@ -37,16 +37,18 @@ 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.thirdparty.guava.common.collect.Sets;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.communication.JSONSerializer;
+import com.vaadin.client.connectors.AbstractRendererConnector;
+import com.vaadin.client.metadata.TypeDataStore.MethodAttribute;
import com.vaadin.client.ui.UnknownComponentConnector;
import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.ui.Connect;
+import elemental.json.JsonValue;
public class ConnectorBundle {
private static final String FAIL_IF_NOT_SERIALIZABLE = "vFailIfNotSerializable";
@@ -59,6 +61,7 @@ public class ConnectorBundle {
private final Set<JType> hasSerializeSupport = new HashSet<JType>();
private final Set<JType> needsSerializeSupport = new HashSet<JType>();
private final Map<JType, GeneratedSerializer> serializers = new HashMap<JType, GeneratedSerializer>();
+ private final Map<JClassType, JType> presentationTypes = new HashMap<JClassType, JType>();
private final Set<JClassType> needsSuperClass = new HashSet<JClassType>();
private final Set<JClassType> needsGwtConstructor = new HashSet<JClassType>();
@@ -69,9 +72,10 @@ public class ConnectorBundle {
private final Map<JClassType, Set<JMethod>> needsReturnType = new HashMap<JClassType, Set<JMethod>>();
private final Map<JClassType, Set<JMethod>> needsInvoker = new HashMap<JClassType, Set<JMethod>>();
private final Map<JClassType, Set<JMethod>> needsParamTypes = new HashMap<JClassType, Set<JMethod>>();
- private final Map<JClassType, Set<JMethod>> needsDelayedInfo = new HashMap<JClassType, Set<JMethod>>();
private final Map<JClassType, Set<JMethod>> needsOnStateChange = new HashMap<JClassType, Set<JMethod>>();
+ private final Map<JClassType, Map<JMethod, Set<MethodAttribute>>> methodAttributes = new HashMap<JClassType, Map<JMethod, Set<MethodAttribute>>>();
+
private final Set<Property> needsProperty = new HashSet<Property>();
private final Map<JClassType, Set<Property>> needsDelegateToWidget = new HashMap<JClassType, Set<Property>>();
@@ -102,7 +106,7 @@ public class ConnectorBundle {
.getName());
JType[] deserializeParamTypes = new JType[] {
oracle.findType(com.vaadin.client.metadata.Type.class.getName()),
- oracle.findType(JSONValue.class.getName()),
+ oracle.findType(JsonValue.class.getName()),
oracle.findType(ApplicationConnection.class.getName()) };
String deserializeMethodName = "deserialize";
// Just test that the method exists
@@ -306,6 +310,25 @@ public class ConnectorBundle {
return Collections.unmodifiableMap(serializers);
}
+ public void setPresentationType(JClassType type, JType presentationType) {
+ if (!hasPresentationType(type)) {
+ presentationTypes.put(type, presentationType);
+ }
+ }
+
+ private boolean hasPresentationType(JClassType type) {
+ if (presentationTypes.containsKey(type)) {
+ return true;
+ } else {
+ return previousBundle != null
+ && previousBundle.hasPresentationType(type);
+ }
+ }
+
+ public Map<JClassType, JType> getPresentationTypes() {
+ return Collections.unmodifiableMap(presentationTypes);
+ }
+
private void setNeedsSuperclass(JClassType typeAsClass) {
if (!isNeedsSuperClass(typeAsClass)) {
needsSuperClass.add(typeAsClass);
@@ -415,6 +438,11 @@ public class ConnectorBundle {
return isConnected(type) && isType(type, ComponentConnector.class);
}
+ public static boolean isConnectedRendererConnector(JClassType type) {
+ return isConnected(type)
+ && isType(type, AbstractRendererConnector.class);
+ }
+
private static boolean isInterfaceType(JClassType type, Class<?> class1) {
return type.isInterface() != null && isType(type, class1);
}
@@ -498,23 +526,35 @@ public class ConnectorBundle {
return Collections.unmodifiableSet(needsProxySupport);
}
- public void setNeedsDelayedInfo(JClassType type, JMethod method) {
- if (!isNeedsDelayedInfo(type, method)) {
- addMapping(needsDelayedInfo, type, method);
+ public void setMethodAttribute(JClassType type, JMethod method,
+ MethodAttribute methodAttribute) {
+ if (!hasMethodAttribute(type, method, methodAttribute)) {
+ Map<JMethod, Set<MethodAttribute>> typeData = methodAttributes
+ .get(type);
+ if (typeData == null) {
+ typeData = new HashMap<JMethod, Set<MethodAttribute>>();
+ methodAttributes.put(type, typeData);
+ }
+
+ addMapping(typeData, method, methodAttribute);
}
}
- private boolean isNeedsDelayedInfo(JClassType type, JMethod method) {
- if (hasMapping(needsDelayedInfo, type, method)) {
+ private boolean hasMethodAttribute(JClassType type, JMethod method,
+ MethodAttribute methodAttribute) {
+ Map<JMethod, Set<MethodAttribute>> typeData = methodAttributes
+ .get(type);
+ if (typeData != null && hasMapping(typeData, method, methodAttribute)) {
return true;
} else {
return previousBundle != null
- && previousBundle.isNeedsDelayedInfo(type, method);
+ && previousBundle.hasMethodAttribute(type, method,
+ methodAttribute);
}
}
- public Map<JClassType, Set<JMethod>> getNeedsDelayedInfo() {
- return Collections.unmodifiableMap(needsDelayedInfo);
+ public Map<JClassType, Map<JMethod, Set<MethodAttribute>>> getMethodAttributes() {
+ return Collections.unmodifiableMap(methodAttributes);
}
public void setNeedsSerialize(JType type) {
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java
index 18e9652ed1..9876baf946 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/EnumSerializer.java
@@ -19,9 +19,10 @@ package com.vaadin.server.widgetsetutils.metadata;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JEnumConstant;
import com.google.gwt.core.ext.typeinfo.JEnumType;
-import com.google.gwt.json.client.JSONString;
import com.google.gwt.user.rebind.SourceWriter;
+import elemental.json.Json;
+
public class EnumSerializer extends JsonSerializer {
private final JEnumType enumType;
@@ -34,8 +35,7 @@ public class EnumSerializer extends JsonSerializer {
@Override
protected void printDeserializerBody(TreeLogger logger, SourceWriter w,
String type, String jsonValue, String connection) {
- w.println("String enumIdentifier = ((" + JSONString.class.getName()
- + ")" + jsonValue + ").stringValue();");
+ w.println("String enumIdentifier = " + jsonValue + ".asString();");
for (JEnumConstant e : enumType.getEnumConstants()) {
w.println("if (\"" + e.getName() + "\".equals(enumIdentifier)) {");
w.indent();
@@ -50,8 +50,8 @@ public class EnumSerializer extends JsonSerializer {
@Override
protected void printSerializerBody(TreeLogger logger, SourceWriter w,
String value, String applicationConnection) {
- // return new JSONString(castedValue.name());
- w.println("return new " + JSONString.class.getName() + "(" + value
+ // return Json.create(castedValue.name());
+ w.println("return " + Json.class.getName() + ".create(" + value
+ ".name());");
}
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java
index 0509689850..a7a6c568da 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/JsonSerializer.java
@@ -19,10 +19,10 @@ package com.vaadin.server.widgetsetutils.metadata;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JType;
-import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.rebind.SourceWriter;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.communication.JSONSerializer;
+import elemental.json.JsonValue;
public abstract class JsonSerializer implements GeneratedSerializer {
@@ -51,7 +51,7 @@ public abstract class JsonSerializer implements GeneratedSerializer {
protected void writeSerializerBody(TreeLogger logger, SourceWriter w) {
String qualifiedSourceName = type.getQualifiedSourceName();
- w.println("public " + JSONValue.class.getName() + " serialize("
+ w.println("public " + JsonValue.class.getName() + " serialize("
+ qualifiedSourceName + " value, "
+ ApplicationConnection.class.getName() + " connection) {");
w.indent();
@@ -69,7 +69,7 @@ public abstract class JsonSerializer implements GeneratedSerializer {
// T deserialize(Type type, JSONValue jsonValue, ApplicationConnection
// connection);
w.println("public " + qualifiedSourceName + " deserialize(Type type, "
- + JSONValue.class.getName() + " jsonValue, "
+ + JsonValue.class.getName() + " jsonValue, "
+ ApplicationConnection.class.getName() + " connection) {");
w.indent();
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java
new file mode 100644
index 0000000000..b0b947e3bf
--- /dev/null
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.server.widgetsetutils.metadata;
+
+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.vaadin.client.connectors.AbstractRendererConnector;
+
+/**
+ * Generates type data for renderer connectors.
+ * <ul>
+ * <li>Stores the return type of the overridden
+ * {@link AbstractRendererConnector#getRenderer() getRenderer} method to enable
+ * automatic creation of an instance of the proper renderer type.
+ * <li>Stores the presentation type of the connector to enable the
+ * {@link AbstractRendererConnector#decode(elemental.json.JsonValue) decode}
+ * method to work without having to implement a "getPresentationType" method.
+ * </ul>
+ *
+ * @see WidgetInitVisitor
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RendererVisitor extends TypeVisitor {
+
+ @Override
+ public void visitConnector(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) throws UnableToCompleteException {
+ if (ConnectorBundle.isConnectedRendererConnector(type)) {
+ doRendererType(logger, type, bundle);
+ doPresentationType(logger, type, bundle);
+ }
+ }
+
+ private static void doRendererType(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) {
+ // The class in which createRenderer is implemented
+ JClassType createRendererClass = ConnectorBundle.findInheritedMethod(
+ type, "createRenderer").getEnclosingType();
+
+ // Needs GWT constructor if createRenderer is not overridden
+ if (createRendererClass.getQualifiedSourceName().equals(
+ AbstractRendererConnector.class.getCanonicalName())) {
+
+ JMethod getRenderer = ConnectorBundle.findInheritedMethod(type,
+ "getRenderer");
+ JClassType rendererType = getRenderer.getReturnType().isClass();
+
+ bundle.setNeedsGwtConstructor(rendererType);
+
+ // Also needs renderer type to find the right GWT constructor
+ bundle.setNeedsReturnType(type, getRenderer);
+
+ logger.log(Type.DEBUG, "Renderer type of " + type + " is "
+ + rendererType);
+ }
+ }
+
+ private void doPresentationType(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) throws UnableToCompleteException {
+ JType presentationType = getPresentationType(type, logger);
+ bundle.setPresentationType(type, presentationType);
+
+ logger.log(Type.DEBUG, "Presentation type of " + type + " is "
+ + presentationType);
+ }
+
+ private static JType getPresentationType(JClassType type, TreeLogger logger)
+ throws UnableToCompleteException {
+ JClassType originalType = type;
+ while (type != null) {
+ if (type.getQualifiedBinaryName().equals(
+ AbstractRendererConnector.class.getName())) {
+ JParameterizedType parameterized = type.isParameterized();
+ if (parameterized == null) {
+ logger.log(
+ Type.ERROR,
+ type.getQualifiedSourceName()
+ + " must define the generic parameter of the inherited "
+ + AbstractRendererConnector.class
+ .getSimpleName());
+ throw new UnableToCompleteException();
+ }
+ return parameterized.getTypeArgs()[0];
+ }
+ type = type.getSuperclass();
+ }
+ throw new IllegalArgumentException("The type "
+ + originalType.getQualifiedSourceName() + " does not extend "
+ + AbstractRendererConnector.class.getName());
+ }
+}
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java
index 6ad0d2fd98..86ece28041 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ServerRpcVisitor.java
@@ -23,6 +23,9 @@ 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.vaadin.client.metadata.TypeDataStore.MethodAttribute;
+import com.vaadin.shared.annotations.NoLoadingIndicator;
+import com.vaadin.shared.annotations.Delayed;
public class ServerRpcVisitor extends TypeVisitor {
@Override
@@ -38,7 +41,22 @@ public class ServerRpcVisitor extends TypeVisitor {
JMethod[] methods = subType.getMethods();
for (JMethod method : methods) {
ClientRpcVisitor.checkReturnType(logger, method);
- bundle.setNeedsDelayedInfo(type, method);
+
+ Delayed delayed = method.getAnnotation(Delayed.class);
+ if (delayed != null) {
+ bundle.setMethodAttribute(type, method,
+ MethodAttribute.DELAYED);
+ if (delayed.lastOnly()) {
+ bundle.setMethodAttribute(type, method,
+ MethodAttribute.LAST_ONLY);
+ }
+ }
+
+ if (method.getAnnotation(NoLoadingIndicator.class) != null) {
+ bundle.setMethodAttribute(type, method,
+ MethodAttribute.NO_LOADING_INDICATOR);
+ }
+
bundle.setNeedsParamTypes(type, method);
JType[] parameterTypes = method.getParameterTypes();
diff --git a/client/build.xml b/client/build.xml
index 19ec05b28a..1e65dc37c5 100644
--- a/client/build.xml
+++ b/client/build.xml
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
-<project name="vaadin-client" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-client" basedir="." default="publish-local">
<description>
Compiles build helpers used when building other
modules.
@@ -18,25 +18,30 @@
<!-- Could possibly compile GWT files also here to verify that a)
the same dependencies are used and b) all dependencies have been declared -->
<fileset file="${gwt.user.jar}" />
+ <fileset file="${gwt.elemental.jar}" />
</path>
<path id="classpath.test.custom" />
<target name="jar">
- <property name="jar.file" location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" />
+ <property name="jar.file"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" />
<antcall target="common.jar">
<reference refid="client.gwt.includes" torefid="extra.jar.includes" />
</antcall>
<jar destfile="${jar.file}" update="true">
<manifest>
- <attribute name="Vaadin-Package-Version" value="1" />
+ <attribute name="Vaadin-Package-Version"
+ value="1" />
<attribute name="Vaadin-Widgetsets" value="com.vaadin.DefaultWidgetSet" />
</manifest>
</jar>
<!-- Hack to add validation dependency with source classifier -->
- <property name="pom.xml" location="${result.dir}/lib/${module.name}-${vaadin.version}.pom" />
+ <property name="pom.xml"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.pom" />
<copy file="${pom.xml}" tofile="${temp.pom}">
<filterchain>
- <replacestring from=" &lt;/dependencies&gt;" to=" &lt;dependency&gt;
+ <replacestring from=" &lt;/dependencies&gt;"
+ to=" &lt;dependency&gt;
&lt;groupId&gt;javax.validation&lt;/groupId&gt;
&lt;artifactId&gt;validation-api&lt;/artifactId&gt;
&lt;version&gt;1.0.0.GA&lt;/version&gt;
diff --git a/client/ivy.xml b/client/ivy.xml
index 3abdcf9ba5..6b941af818 100644
--- a/client/ivy.xml
+++ b/client/ivy.xml
@@ -42,6 +42,9 @@
<dependency org="javax.validation" name="validation-api"
rev="1.0.0.GA" conf="build->default,sources" />
+ <!-- Testing dependencies -->
+ <dependency org="org.easymock" name="easymock" rev="3.0"
+ conf="test,ide-> default" />
</dependencies>
diff --git a/client/src/com/vaadin/DefaultWidgetSet.gwt.xml b/client/src/com/vaadin/DefaultWidgetSet.gwt.xml
index 8512d547e3..3047924ac7 100755
--- a/client/src/com/vaadin/DefaultWidgetSet.gwt.xml
+++ b/client/src/com/vaadin/DefaultWidgetSet.gwt.xml
@@ -8,8 +8,23 @@
<inherits name="com.vaadin.Vaadin" />
+ <!-- Elemental is used for handling Json only -->
+ <inherits name="elemental.Json" />
+
<entry-point class="com.vaadin.client.ApplicationConfiguration" />
+ <generate-with
+ class="com.vaadin.server.widgetsetutils.AcceptCriteriaFactoryGenerator">
+ <when-type-is class="com.vaadin.client.ui.dd.VAcceptCriterionFactory" />
+ </generate-with>
+
+ <generate-with
+ class="com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory">
+ <when-type-assignable
+ class="com.vaadin.client.metadata.ConnectorBundleLoader" />
+ </generate-with>
+
+
<!-- Since 7.2. Compile all permutations (browser support) into one Javascript
file. Speeds up compilation and does not make the Javascript significantly
larger. -->
diff --git a/client/src/com/vaadin/Vaadin.gwt.xml b/client/src/com/vaadin/Vaadin.gwt.xml
index 5e8f08fe22..1bbece6bd6 100644
--- a/client/src/com/vaadin/Vaadin.gwt.xml
+++ b/client/src/com/vaadin/Vaadin.gwt.xml
@@ -10,8 +10,6 @@
<inherits name="com.google.gwt.http.HTTP" />
- <inherits name="com.google.gwt.json.JSON" />
-
<inherits name="com.google.gwt.logging.Logging" />
<set-property name="gwt.logging.enabled" value="TRUE" />
@@ -26,17 +24,6 @@
<when-type-is class="com.google.gwt.core.client.impl.SchedulerImpl" />
</replace-with>
- <generate-with
- class="com.vaadin.server.widgetsetutils.AcceptCriteriaFactoryGenerator">
- <when-type-is class="com.vaadin.client.ui.dd.VAcceptCriterionFactory" />
- </generate-with>
-
- <generate-with
- class="com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory">
- <when-type-assignable
- class="com.vaadin.client.metadata.ConnectorBundleLoader" />
- </generate-with>
-
<replace-with
class="com.vaadin.client.communication.AtmospherePushConnection">
<when-type-is class="com.vaadin.client.communication.PushConnection" />
@@ -92,4 +79,9 @@
<when-type-is class="com.vaadin.client.event.PointerEventSupportImpl" />
<when-property-is value="ie10" name="user.agent" />
</replace-with>
+
+ <!-- Make Sass linking available -->
+ <define-linker name="sass"
+ class="com.vaadin.sass.linker.SassLinker" />
+
</module>
diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java
index 5eeeabe743..37d689c6d3 100644
--- a/client/src/com/vaadin/client/ApplicationConfiguration.java
+++ b/client/src/com/vaadin/client/ApplicationConfiguration.java
@@ -406,14 +406,14 @@ public class ApplicationConfiguration implements EntryPoint {
* desired locations even if the base URL of the page changes later
* (e.g. with pushState)
*/
- serviceUrl = Util.getAbsoluteUrl(serviceUrl);
+ serviceUrl = WidgetUtil.getAbsoluteUrl(serviceUrl);
}
// Ensure there's an ending slash (to make appending e.g. UIDL work)
if (!useServiceUrlPathParam() && !serviceUrl.endsWith("/")) {
serviceUrl += '/';
}
- vaadinDirUrl = Util.getAbsoluteUrl(jsoConfiguration
+ vaadinDirUrl = WidgetUtil.getAbsoluteUrl(jsoConfiguration
.getConfigString(ApplicationConstants.VAADIN_DIR_URL));
uiId = jsoConfiguration.getConfigInteger(UIConstants.UI_ID_PARAMETER)
.intValue();
diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java
index 0ac2f4312d..140c1dbfef 100644
--- a/client/src/com/vaadin/client/ApplicationConnection.java
+++ b/client/src/com/vaadin/client/ApplicationConnection.java
@@ -51,10 +51,6 @@ 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.JSONNumber;
-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;
@@ -66,6 +62,7 @@ import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConfiguration.ErrorMessage;
+import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent;
import com.vaadin.client.ResourceLoader.ResourceLoadEvent;
import com.vaadin.client.ResourceLoader.ResourceLoadListener;
import com.vaadin.client.communication.HasJavaScriptConnectorHelper;
@@ -84,6 +81,7 @@ import com.vaadin.client.metadata.NoDataException;
import com.vaadin.client.metadata.Property;
import com.vaadin.client.metadata.Type;
import com.vaadin.client.metadata.TypeData;
+import com.vaadin.client.metadata.TypeDataStore;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.AbstractConnector;
import com.vaadin.client.ui.FontIcon;
@@ -108,6 +106,11 @@ import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.shared.ui.ui.UIState.PushConfigurationState;
import com.vaadin.shared.util.SharedUtil;
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
/**
* This is the client side communication "engine", managing client-server
* communication with its server side counterpart
@@ -144,15 +147,20 @@ public class ApplicationConnection implements HasHandlers {
private FastStringSet detachedConnectorIds = FastStringSet.create();
}
- public static final String MODIFIED_CLASSNAME = "v-modified";
+ @Deprecated
+ public static final String MODIFIED_CLASSNAME = StyleConstants.MODIFIED;
- public static final String DISABLED_CLASSNAME = "v-disabled";
+ @Deprecated
+ public static final String DISABLED_CLASSNAME = StyleConstants.DISABLED;
- public static final String REQUIRED_CLASSNAME = "v-required";
+ @Deprecated
+ public static final String REQUIRED_CLASSNAME = StyleConstants.REQUIRED;
- public static final String REQUIRED_CLASSNAME_EXT = "-required";
+ @Deprecated
+ public static final String REQUIRED_CLASSNAME_EXT = StyleConstants.REQUIRED_EXT;
- public static final String ERROR_CLASSNAME_EXT = "-error";
+ @Deprecated
+ public static final String ERROR_CLASSNAME_EXT = StyleConstants.ERROR_EXT;
/**
* A string that, if found in a non-JSON response to a UIDL request, will
@@ -796,7 +804,7 @@ public class ApplicationConnection implements HasHandlers {
}
protected void repaintAll() {
- makeUidlRequest(new JSONArray(), getRepaintAllParameters());
+ makeUidlRequest(Json.createArray(), getRepaintAllParameters());
}
/**
@@ -835,21 +843,19 @@ public class ApplicationConnection implements HasHandlers {
* no parameters should be added. Should not start with any
* special character.
*/
- protected void makeUidlRequest(final JSONArray reqInvocations,
+ protected void makeUidlRequest(final JsonArray reqInvocations,
final String extraParams) {
startRequest();
- JSONObject payload = new JSONObject();
+ JsonObject payload = Json.createObject();
if (!getCsrfToken().equals(
ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE)) {
- payload.put(ApplicationConstants.CSRF_TOKEN, new JSONString(
- getCsrfToken()));
+ payload.put(ApplicationConstants.CSRF_TOKEN, getCsrfToken());
}
payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations);
- payload.put(ApplicationConstants.SERVER_SYNC_ID, new JSONNumber(
- lastSeenServerSyncId));
+ payload.put(ApplicationConstants.SERVER_SYNC_ID, lastSeenServerSyncId);
- VConsole.log("Making UIDL Request with params: " + payload);
+ VConsole.log("Making UIDL Request with params: " + payload.toJson());
String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX
+ ApplicationConstants.UIDL_PATH + '/');
@@ -872,7 +878,7 @@ public class ApplicationConnection implements HasHandlers {
* @param payload
* The contents of the request to send
*/
- protected void doUidlRequest(final String uri, final JSONObject payload) {
+ protected void doUidlRequest(final String uri, final JsonObject payload) {
doUidlRequest(uri, payload, true);
}
@@ -888,7 +894,7 @@ public class ApplicationConnection implements HasHandlers {
* true when a status code 0 should be retried
* @since 7.3.7
*/
- protected void doUidlRequest(final String uri, final JSONObject payload,
+ protected void doUidlRequest(final String uri, final JsonObject payload,
final boolean retry) {
RequestCallback requestCallback = new RequestCallback() {
@Override
@@ -1073,14 +1079,14 @@ public class ApplicationConnection implements HasHandlers {
* @throws RequestException
* if the request could not be sent
*/
- protected void doAjaxRequest(String uri, JSONObject payload,
+ protected void doAjaxRequest(String uri, JsonObject payload,
RequestCallback requestCallback) throws RequestException {
RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
// TODO enable timeout
// rb.setTimeoutMillis(timeoutMillis);
// TODO this should be configurable
rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE);
- rb.setRequestData(payload.toString());
+ rb.setRequestData(payload.toJson());
rb.setCallback(requestCallback);
final Request request = rb.send();
@@ -1301,7 +1307,6 @@ public class ApplicationConnection implements HasHandlers {
}
hasActiveRequest = true;
requestStartTime = new Date();
- loadingIndicator.trigger();
eventBus.fireEvent(new RequestStartingEvent(this));
}
@@ -1326,7 +1331,8 @@ public class ApplicationConnection implements HasHandlers {
Scheduler.get().scheduleDeferred(new Command() {
@Override
public void execute() {
- if (!hasActiveRequest()) {
+ if (!isApplicationRunning()
+ || !(hasActiveRequest() || deferredSendPending)) {
getLoadingIndicator().hide();
// If on Liferay and session expiration management is in
@@ -1591,6 +1597,8 @@ public class ApplicationConnection implements HasHandlers {
}
Command c = new Command() {
+ private boolean onlyNoLayoutUpdates = true;
+
@Override
public void execute() {
assert syncId == -1 || syncId == lastSeenServerSyncId;
@@ -1684,15 +1692,17 @@ public class ApplicationConnection implements HasHandlers {
updatingState = false;
- Profiler.enter("Layout processing");
- try {
- LayoutManager layoutManager = getLayoutManager();
- layoutManager.setEverythingNeedsMeasure();
- layoutManager.layoutNow();
- } catch (final Throwable e) {
- VConsole.error(e);
+ if (!onlyNoLayoutUpdates) {
+ Profiler.enter("Layout processing");
+ try {
+ LayoutManager layoutManager = getLayoutManager();
+ layoutManager.setEverythingNeedsMeasure();
+ layoutManager.layoutNow();
+ } catch (final Throwable e) {
+ VConsole.error(e);
+ }
+ Profiler.leave("Layout processing");
}
- Profiler.leave("Layout processing");
if (ApplicationConfiguration.isDebugMode()) {
Profiler.enter("Dumping state changes to the console");
@@ -1763,11 +1773,12 @@ public class ApplicationConnection implements HasHandlers {
// Create fake server response that says that the uiConnector
// has no children
- JSONObject fakeHierarchy = new JSONObject();
- fakeHierarchy.put(uiConnectorId, new JSONArray());
- JSONObject fakeJson = new JSONObject();
+ JsonObject fakeHierarchy = Json.createObject();
+ fakeHierarchy.put(uiConnectorId, Json.createArray());
+ JsonObject fakeJson = Json.createObject();
fakeJson.put("hierarchy", fakeHierarchy);
- ValueMap fakeValueMap = fakeJson.getJavaScriptObject().cast();
+ ValueMap fakeValueMap = ((JavaScriptObject) fakeJson.toNative())
+ .cast();
// Update hierarchy based on the fake response
ConnectorHierarchyUpdateResult connectorHierarchyUpdateResult = updateConnectorHierarchy(fakeValueMap);
@@ -1896,7 +1907,7 @@ public class ApplicationConnection implements HasHandlers {
} catch (NoDataException e) {
throw new RuntimeException(
"Missing data needed to invoke @DelegateToWidget for "
- + Util.getSimpleName(component), e);
+ + component.getClass().getSimpleName(), e);
}
}
@@ -2012,6 +2023,11 @@ public class ApplicationConnection implements HasHandlers {
if (connector != null) {
continue;
}
+
+ // Always do layouts if there's at least one new
+ // connector
+ onlyNoLayoutUpdates = false;
+
int connectorType = Integer.parseInt(types
.getString(connectorId));
@@ -2053,6 +2069,11 @@ public class ApplicationConnection implements HasHandlers {
JsArray<ValueMap> changes = json.getJSValueMapArray("changes");
int length = changes.length();
+ // Must always do layout if there's even a single legacy update
+ if (length != 0) {
+ onlyNoLayoutUpdates = false;
+ }
+
VConsole.log(" * Passing UIDL to Vaadin 6 style connectors");
// update paintables
for (int i = 0; i < length; i++) {
@@ -2067,7 +2088,8 @@ public class ApplicationConnection implements HasHandlers {
String key = null;
if (Profiler.isEnabled()) {
key = "updateFromUIDL for "
- + Util.getSimpleName(legacyConnector);
+ + legacyConnector.getClass()
+ .getSimpleName();
Profiler.enter(key);
}
@@ -2167,30 +2189,44 @@ public class ApplicationConnection implements HasHandlers {
Profiler.enter("updateConnectorState inner loop");
if (Profiler.isEnabled()) {
Profiler.enter("Decode connector state "
- + Util.getSimpleName(connector));
+ + connector.getClass().getSimpleName());
}
- JSONObject stateJson = new JSONObject(
- states.getJavaScriptObject(connectorId));
+ JavaScriptObject jso = states
+ .getJavaScriptObject(connectorId);
+ JsonObject stateJson = Util.jso2json(jso);
if (connector instanceof HasJavaScriptConnectorHelper) {
((HasJavaScriptConnectorHelper) connector)
.getJavascriptConnectorHelper()
- .setNativeState(
- stateJson.getJavaScriptObject());
+ .setNativeState(jso);
}
SharedState state = connector.getState();
+ Type stateType = new Type(state.getClass()
+ .getName(), null);
+
+ if (onlyNoLayoutUpdates) {
+ Profiler.enter("updateConnectorState @NoLayout handling");
+ for (String propertyName : stateJson.keys()) {
+ Property property = stateType
+ .getProperty(propertyName);
+ if (!property.isNoLayout()) {
+ onlyNoLayoutUpdates = false;
+ break;
+ }
+ }
+ Profiler.leave("updateConnectorState @NoLayout handling");
+ }
Profiler.enter("updateConnectorState decodeValue");
- JsonDecoder.decodeValue(new Type(state.getClass()
- .getName(), null), stateJson, state,
- ApplicationConnection.this);
+ JsonDecoder.decodeValue(stateType, stateJson,
+ state, ApplicationConnection.this);
Profiler.leave("updateConnectorState decodeValue");
if (Profiler.isEnabled()) {
Profiler.leave("Decode connector state "
- + Util.getSimpleName(connector));
+ + connector.getClass().getSimpleName());
}
Profiler.enter("updateConnectorState create event");
@@ -2224,7 +2260,7 @@ public class ApplicationConnection implements HasHandlers {
.getConnector(connectorId);
StateChangeEvent event = new StateChangeEvent(connector,
- new JSONObject(), true);
+ Json.createObject(), true);
events.add(event);
@@ -2403,6 +2439,10 @@ public class ApplicationConnection implements HasHandlers {
Profiler.leave("updateConnectorHierarchy detach removed connectors");
+ if (result.events.size() != 0) {
+ onlyNoLayoutUpdates = false;
+ }
+
Profiler.leave("updateConnectorHierarchy");
return result;
@@ -2526,15 +2566,23 @@ public class ApplicationConnection implements HasHandlers {
VConsole.log(" * Performing server to client RPC calls");
- JSONArray rpcCalls = new JSONArray(
- json.getJavaScriptObject("rpc"));
+ JsonArray rpcCalls = Util.jso2json(json
+ .getJavaScriptObject("rpc"));
- int rpcLength = rpcCalls.size();
+ int rpcLength = rpcCalls.length();
for (int i = 0; i < rpcLength; i++) {
try {
- JSONArray rpcCall = (JSONArray) rpcCalls.get(i);
- rpcManager.parseAndApplyInvocation(rpcCall,
- ApplicationConnection.this);
+ JsonArray rpcCall = rpcCalls.getArray(i);
+ MethodInvocation invocation = rpcManager
+ .parseAndApplyInvocation(rpcCall,
+ ApplicationConnection.this);
+
+ if (onlyNoLayoutUpdates
+ && !RpcManager.getMethod(invocation)
+ .isNoLayout()) {
+ onlyNoLayoutUpdates = false;
+ }
+
} catch (final Throwable e) {
VConsole.error(e);
}
@@ -2708,8 +2756,8 @@ public class ApplicationConnection implements HasHandlers {
*
*/
public void sendPendingVariableChanges() {
- if (!deferedSendPending) {
- deferedSendPending = true;
+ if (!deferredSendPending) {
+ deferredSendPending = true;
Scheduler.get().scheduleFinally(sendPendingCommand);
}
}
@@ -2717,11 +2765,11 @@ public class ApplicationConnection implements HasHandlers {
private final ScheduledCommand sendPendingCommand = new ScheduledCommand() {
@Override
public void execute() {
- deferedSendPending = false;
+ deferredSendPending = false;
doSendPendingVariableChanges();
}
};
- private boolean deferedSendPending = false;
+ private boolean deferredSendPending = false;
private void doSendPendingVariableChanges() {
if (isApplicationRunning()) {
@@ -2756,22 +2804,19 @@ public class ApplicationConnection implements HasHandlers {
*/
private void buildAndSendVariableBurst(
LinkedHashMap<String, MethodInvocation> pendingInvocations) {
-
- JSONArray reqJson = new JSONArray();
+ boolean showLoadingIndicator = false;
+ JsonArray reqJson = Json.createArray();
if (!pendingInvocations.isEmpty()) {
if (ApplicationConfiguration.isDebugMode()) {
Util.logVariableBurst(this, pendingInvocations.values());
}
for (MethodInvocation invocation : pendingInvocations.values()) {
- 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();
+ JsonArray invocationJson = Json.createArray();
+ invocationJson.set(0, invocation.getConnectorId());
+ invocationJson.set(1, invocation.getInterfaceName());
+ invocationJson.set(2, invocation.getMethodName());
+ JsonArray paramJson = Json.createArray();
Type[] parameterTypes = null;
if (!isLegacyVariableChange(invocation)
@@ -2782,10 +2827,16 @@ public class ApplicationConnection implements HasHandlers {
Method method = type.getMethod(invocation
.getMethodName());
parameterTypes = method.getParameterTypes();
+
+ showLoadingIndicator |= !TypeDataStore
+ .isNoLoadingIndicator(method);
} catch (NoDataException e) {
throw new RuntimeException("No type data for "
+ invocation.toString(), e);
}
+ } else {
+ // Always show loading indicator for legacy requests
+ showLoadingIndicator = true;
}
for (int i = 0; i < invocation.getParameters().length; ++i) {
@@ -2795,10 +2846,11 @@ public class ApplicationConnection implements HasHandlers {
type = parameterTypes[i];
}
Object value = invocation.getParameters()[i];
- paramJson.set(i, JsonEncoder.encode(value, type, this));
+ JsonValue jsonValue = JsonEncoder.encode(value, type, this);
+ paramJson.set(i, jsonValue);
}
invocationJson.set(3, paramJson);
- reqJson.set(reqJson.size(), invocationJson);
+ reqJson.set(reqJson.length(), invocationJson);
}
pendingInvocations.clear();
@@ -2816,6 +2868,9 @@ public class ApplicationConnection implements HasHandlers {
getConfiguration().setWidgetsetVersionSent();
}
+ if (showLoadingIndicator) {
+ getLoadingIndicator().trigger();
+ }
makeUidlRequest(reqJson, extraParams);
}
@@ -3562,7 +3617,7 @@ public class ApplicationConnection implements HasHandlers {
* @return Connector for focused element or null.
*/
private ComponentConnector getActiveConnector() {
- Element focusedElement = Util.getFocusedElement();
+ Element focusedElement = WidgetUtil.getFocusedElement();
if (focusedElement == null) {
return null;
}
diff --git a/client/src/com/vaadin/client/BrowserInfo.java b/client/src/com/vaadin/client/BrowserInfo.java
index e8b8d8309a..5ca79cb121 100644
--- a/client/src/com/vaadin/client/BrowserInfo.java
+++ b/client/src/com/vaadin/client/BrowserInfo.java
@@ -343,7 +343,7 @@ public class BrowserInfo {
public boolean requiresOverflowAutoFix() {
return (getWebkitVersion() > 0 || getOperaVersion() >= 11
|| getIEVersion() >= 10 || isFirefox())
- && Util.getNativeScrollbarSize() > 0;
+ && WidgetUtil.getNativeScrollbarSize() > 0;
}
/**
@@ -359,7 +359,8 @@ public class BrowserInfo {
* otherwise <code>false</code>
*/
public boolean requiresPositionAbsoluteOverflowAutoFix() {
- return (getWebkitVersion() > 0) && Util.getNativeScrollbarSize() > 0;
+ return (getWebkitVersion() > 0)
+ && WidgetUtil.getNativeScrollbarSize() > 0;
}
/**
diff --git a/client/src/com/vaadin/client/JavaScriptConnectorHelper.java b/client/src/com/vaadin/client/JavaScriptConnectorHelper.java
index c4b36d6453..1eb326115e 100644
--- a/client/src/com/vaadin/client/JavaScriptConnectorHelper.java
+++ b/client/src/com/vaadin/client/JavaScriptConnectorHelper.java
@@ -26,7 +26,6 @@ import java.util.Set;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.dom.client.Element;
-import com.google.gwt.json.client.JSONArray;
import com.vaadin.client.communication.JavaScriptMethodInvocation;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
@@ -35,6 +34,8 @@ import com.vaadin.client.ui.layout.ElementResizeListener;
import com.vaadin.shared.JavaScriptConnectorState;
import com.vaadin.shared.communication.MethodInvocation;
+import elemental.json.JsonArray;
+
public class JavaScriptConnectorHelper {
private final ServerConnector connector;
@@ -49,7 +50,7 @@ public class JavaScriptConnectorHelper {
private JavaScriptObject connectorWrapper;
private int tag;
- private boolean inited = false;
+ private String initFunctionName;
public JavaScriptConnectorHelper(ServerConnector connector) {
this.connector = connector;
@@ -58,51 +59,79 @@ public class JavaScriptConnectorHelper {
rpcObjects.put("", JavaScriptObject.createObject());
}
+ /**
+ * The id of the previous response for which state changes have been
+ * processed. If this is the same as the
+ * {@link ApplicationConnection#getLastResponseId()}, it means that the
+ * state change has already been handled and should not be done again.
+ */
+ private int processedResponseId = -1;
+
public void init() {
connector.addStateChangeHandler(new StateChangeHandler() {
@Override
public void onStateChanged(StateChangeEvent stateChangeEvent) {
- JavaScriptObject wrapper = getConnectorWrapper();
- JavaScriptConnectorState state = getConnectorState();
+ processStateChanges();
+ }
+ });
+ }
- for (String callback : state.getCallbackNames()) {
- ensureCallback(JavaScriptConnectorHelper.this, wrapper,
- callback);
- }
+ /**
+ * Makes sure the javascript part of the connector has been initialized. The
+ * javascript is usually initalized the first time a state change event is
+ * received, but it might in some cases be necessary to make this happen
+ * earlier.
+ *
+ * @since 7.4.0
+ */
+ public void ensureJavascriptInited() {
+ if (initFunctionName == null) {
+ processStateChanges();
+ }
+ }
+
+ private void processStateChanges() {
+ int lastResponseId = connector.getConnection().getLastResponseId();
+ if (processedResponseId == lastResponseId) {
+ return;
+ }
+ processedResponseId = lastResponseId;
- 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);
- }
+ 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;
- }
+ // Init after setting up callbacks & rpc
+ if (initFunctionName == null) {
+ initJavaScript();
+ }
- invokeIfPresent(wrapper, "onStateChange");
- }
- });
+ invokeIfPresent(wrapper, "onStateChange");
}
private static String getJsInterfaceName(String rpcName) {
@@ -119,7 +148,7 @@ public class JavaScriptConnectorHelper {
return object;
}
- private boolean initJavaScript() {
+ protected boolean initJavaScript() {
ApplicationConfiguration conf = connector.getConnection()
.getConfiguration();
ArrayList<String> attemptedNames = new ArrayList<String>();
@@ -131,6 +160,7 @@ public class JavaScriptConnectorHelper {
if (tryInitJs(initFunctionName, getConnectorWrapper())) {
VConsole.log("JavaScript connector initialized using "
+ initFunctionName);
+ this.initFunctionName = initFunctionName;
return true;
} else {
VConsole.log("No JavaScript function " + initFunctionName
@@ -159,7 +189,7 @@ public class JavaScriptConnectorHelper {
}
}-*/;
- private JavaScriptObject getConnectorWrapper() {
+ public JavaScriptObject getConnectorWrapper() {
if (connectorWrapper == null) {
connectorWrapper = createConnectorWrapper(this,
connector.getConnection(), nativeState, rpcMap,
@@ -318,7 +348,7 @@ public class JavaScriptConnectorHelper {
iface = findWildcardInterface(method);
}
- JSONArray argumentsArray = new JSONArray(arguments);
+ JsonArray argumentsArray = Util.jso2json(arguments);
Object[] parameters = new Object[arguments.length()];
for (int i = 0; i < parameters.length; i++) {
parameters[i] = argumentsArray.get(i);
@@ -355,7 +385,7 @@ public class JavaScriptConnectorHelper {
MethodInvocation invocation = new JavaScriptMethodInvocation(
connector.getConnectorId(),
"com.vaadin.ui.JavaScript$JavaScriptCallbackRpc", "call",
- new Object[] { name, new JSONArray(arguments) });
+ new Object[] { name, arguments });
connector.getConnection().addMethodInvocationToQueue(invocation, false,
false);
}
@@ -381,8 +411,8 @@ public class JavaScriptConnectorHelper {
}
}-*/;
- public Object[] decodeRpcParameters(JSONArray parametersJson) {
- return new Object[] { parametersJson.getJavaScriptObject() };
+ public Object[] decodeRpcParameters(JsonArray parametersJson) {
+ return new Object[] { Util.json2jso(parametersJson) };
}
public void setTag(int tag) {
@@ -390,18 +420,16 @@ public class JavaScriptConnectorHelper {
}
public void invokeJsRpc(MethodInvocation invocation,
- JSONArray parametersJson) {
+ 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();
+ String callbackName = parametersJson.getString(0);
+ JavaScriptObject arguments = Util.json2jso(parametersJson.get(1));
invokeCallback(getConnectorWrapper(), callbackName, arguments);
} else {
- JavaScriptObject arguments = parametersJson.getJavaScriptObject();
+ JavaScriptObject arguments = Util.json2jso(parametersJson);
invokeJsRpc(rpcMap, iface, method, arguments);
// Also invoke wildcard interface
invokeJsRpc(rpcMap, "", method, arguments);
@@ -466,4 +494,7 @@ public class JavaScriptConnectorHelper {
}
}-*/;
+ public String getInitFunctionName() {
+ return initFunctionName;
+ }
}
diff --git a/client/src/com/vaadin/client/LayoutManager.java b/client/src/com/vaadin/client/LayoutManager.java
index 1fee13b16d..9775c29ab6 100644
--- a/client/src/com/vaadin/client/LayoutManager.java
+++ b/client/src/com/vaadin/client/LayoutManager.java
@@ -360,7 +360,8 @@ public class LayoutManager {
if (Profiler.isEnabled()) {
Profiler.enter("ElementResizeListener.onElementResize construct profiler key");
key = "ElementResizeListener.onElementResize for "
- + Util.getSimpleName(listener);
+ + listener.getClass()
+ .getSimpleName();
Profiler.leave("ElementResizeListener.onElementResize construct profiler key");
Profiler.enter(key);
}
@@ -403,7 +404,7 @@ public class LayoutManager {
String key = null;
if (Profiler.isEnabled()) {
key = "layoutHorizontally() for "
- + Util.getSimpleName(cl);
+ + cl.getClass().getSimpleName();
Profiler.enter(key);
}
@@ -425,7 +426,8 @@ public class LayoutManager {
try {
String key = null;
if (Profiler.isEnabled()) {
- key = "layout() for " + Util.getSimpleName(rr);
+ key = "layout() for "
+ + rr.getClass().getSimpleName();
Profiler.enter(key);
}
@@ -458,7 +460,7 @@ public class LayoutManager {
String key = null;
if (Profiler.isEnabled()) {
key = "layoutVertically() for "
- + Util.getSimpleName(cl);
+ + cl.getClass().getSimpleName();
Profiler.enter(key);
}
@@ -480,7 +482,8 @@ public class LayoutManager {
try {
String key = null;
if (Profiler.isEnabled()) {
- key = "layout() for " + Util.getSimpleName(rr);
+ key = "layout() for "
+ + rr.getClass().getSimpleName();
Profiler.enter(key);
}
@@ -559,7 +562,7 @@ public class LayoutManager {
String key = null;
if (Profiler.isEnabled()) {
key = "layout PostLayoutListener for "
- + Util.getSimpleName(connector);
+ + connector.getClass().getSimpleName();
Profiler.enter(key);
}
diff --git a/client/src/com/vaadin/client/LayoutManagerIE8.java b/client/src/com/vaadin/client/LayoutManagerIE8.java
index 941ac589b2..9fb6819e83 100644
--- a/client/src/com/vaadin/client/LayoutManagerIE8.java
+++ b/client/src/com/vaadin/client/LayoutManagerIE8.java
@@ -94,7 +94,7 @@ public class LayoutManagerIE8 extends LayoutManager {
* the containing element. To force a reflow by modifying the magical
* zoom property.
*/
- Util.forceIE8Redraw(RootPanel.get().getElement());
+ WidgetUtil.forceIE8Redraw(RootPanel.get().getElement());
Profiler.leave("LayoutManagerIE8.performBrowserLayoutHacks");
}
}
diff --git a/client/src/com/vaadin/client/MeasuredSize.java b/client/src/com/vaadin/client/MeasuredSize.java
index 2531ff9389..8520635a4d 100644
--- a/client/src/com/vaadin/client/MeasuredSize.java
+++ b/client/src/com/vaadin/client/MeasuredSize.java
@@ -236,7 +236,7 @@ public class MeasuredSize {
Profiler.leave("Measure borders");
Profiler.enter("Measure height");
- int requiredHeight = Util.getRequiredHeight(element);
+ int requiredHeight = WidgetUtil.getRequiredHeight(element);
int marginHeight = sumHeights(margins);
int oldHeight = height;
int oldWidth = width;
@@ -247,7 +247,7 @@ public class MeasuredSize {
Profiler.leave("Measure height");
Profiler.enter("Measure width");
- int requiredWidth = Util.getRequiredWidth(element);
+ int requiredWidth = WidgetUtil.getRequiredWidth(element);
int marginWidth = sumWidths(margins);
if (setOuterWidth(requiredWidth + marginWidth)) {
debugSizeChange(element, "Width (outer)", oldWidth, width);
diff --git a/client/src/com/vaadin/client/MouseEventDetailsBuilder.java b/client/src/com/vaadin/client/MouseEventDetailsBuilder.java
index 313fe682fd..11ebe3925c 100644
--- a/client/src/com/vaadin/client/MouseEventDetailsBuilder.java
+++ b/client/src/com/vaadin/client/MouseEventDetailsBuilder.java
@@ -57,8 +57,8 @@ public class MouseEventDetailsBuilder {
Element relativeToElement) {
MouseEventDetails mouseEventDetails = new MouseEventDetails();
mouseEventDetails.setType(Event.getTypeInt(evt.getType()));
- mouseEventDetails.setClientX(Util.getTouchOrMouseClientX(evt));
- mouseEventDetails.setClientY(Util.getTouchOrMouseClientY(evt));
+ mouseEventDetails.setClientX(WidgetUtil.getTouchOrMouseClientX(evt));
+ mouseEventDetails.setClientY(WidgetUtil.getTouchOrMouseClientY(evt));
if (evt.getButton() == NativeEvent.BUTTON_LEFT) {
mouseEventDetails.setButton(MouseButton.LEFT);
} else if (evt.getButton() == NativeEvent.BUTTON_RIGHT) {
diff --git a/client/src/com/vaadin/client/Profiler.java b/client/src/com/vaadin/client/Profiler.java
index 6c0967099f..4b35427575 100644
--- a/client/src/com/vaadin/client/Profiler.java
+++ b/client/src/com/vaadin/client/Profiler.java
@@ -17,11 +17,13 @@
package com.vaadin.client;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
@@ -29,8 +31,6 @@ import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.shared.GWT;
-import com.vaadin.client.debug.internal.ProfilerSection.Node;
-import com.vaadin.client.debug.internal.ProfilerSection.ProfilerResultConsumer;
/**
* Lightweight profiling tool that can be used to collect profiling data with
@@ -55,6 +55,236 @@ public class Profiler {
}
}
+ /**
+ * Interface for getting data from the {@link Profiler}.
+ * <p>
+ * <b>Warning!</b> This interface is most likely to change in the future
+ *
+ * @since 7.1
+ * @author Vaadin Ltd
+ */
+ public interface ProfilerResultConsumer {
+ public void addProfilerData(Node rootNode, List<Node> totals);
+
+ public void addBootstrapData(LinkedHashMap<String, Double> timings);
+ }
+
+ /**
+ * A hierarchical representation of the time spent running a named block of
+ * code.
+ * <p>
+ * <b>Warning!</b> This class is most likely to change in the future and is
+ * therefore defined in this class in an internal package instead of
+ * Profiler where it might seem more logical.
+ */
+ public static class Node {
+ private final String name;
+ private final LinkedHashMap<String, Node> children = new LinkedHashMap<String, Node>();
+ private double time = 0;
+ private int count = 0;
+ private double enterTime = 0;
+ private double minTime = 1000000000;
+ private double maxTime = 0;
+
+ /**
+ * Create a new node with the given name.
+ *
+ * @param name
+ */
+ public Node(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Gets the name of the node
+ *
+ * @return the name of the node
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Creates a new child node or retrieves and existing child and updates
+ * its total time and hit count.
+ *
+ * @param name
+ * the name of the child
+ * @param timestamp
+ * the timestamp for when the node is entered
+ * @return the child node object
+ */
+ public Node enterChild(String name, double timestamp) {
+ Node child = children.get(name);
+ if (child == null) {
+ child = new Node(name);
+ children.put(name, child);
+ }
+ child.enterTime = timestamp;
+ child.count++;
+ return child;
+ }
+
+ /**
+ * Gets the total time spent in this node, including time spent in sub
+ * nodes
+ *
+ * @return the total time spent, in milliseconds
+ */
+ public double getTimeSpent() {
+ return time;
+ }
+
+ /**
+ * Gets the minimum time spent for one invocation of this node,
+ * including time spent in sub nodes
+ *
+ * @return the time spent for the fastest invocation, in milliseconds
+ */
+ public double getMinTimeSpent() {
+ return minTime;
+ }
+
+ /**
+ * Gets the maximum time spent for one invocation of this node,
+ * including time spent in sub nodes
+ *
+ * @return the time spent for the slowest invocation, in milliseconds
+ */
+ public double getMaxTimeSpent() {
+ return maxTime;
+ }
+
+ /**
+ * Gets the number of times this node has been entered
+ *
+ * @return the number of times the node has been entered
+ */
+ public int getCount() {
+ return count;
+ }
+
+ /**
+ * Gets the total time spent in this node, excluding time spent in sub
+ * nodes
+ *
+ * @return the total time spent, in milliseconds
+ */
+ public double getOwnTime() {
+ double time = getTimeSpent();
+ for (Node node : children.values()) {
+ time -= node.getTimeSpent();
+ }
+ return time;
+ }
+
+ /**
+ * Gets the child nodes of this node
+ *
+ * @return a collection of child nodes
+ */
+ public Collection<Node> getChildren() {
+ return Collections.unmodifiableCollection(children.values());
+ }
+
+ private void buildRecursiveString(StringBuilder builder, String prefix) {
+ if (getName() != null) {
+ String msg = getStringRepresentation(prefix);
+ builder.append(msg + '\n');
+ }
+ String childPrefix = prefix + "*";
+ for (Node node : children.values()) {
+ node.buildRecursiveString(builder, childPrefix);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getStringRepresentation("");
+ }
+
+ public String getStringRepresentation(String prefix) {
+ if (getName() == null) {
+ return "";
+ }
+ String msg = prefix + " " + getName() + " in " + getTimeSpent()
+ + " ms.";
+ if (getCount() > 1) {
+ msg += " Invoked "
+ + getCount()
+ + " times ("
+ + roundToSignificantFigures(getTimeSpent() / getCount())
+ + " ms per time, min "
+ + roundToSignificantFigures(getMinTimeSpent())
+ + " ms, max "
+ + roundToSignificantFigures(getMaxTimeSpent())
+ + " ms).";
+ }
+ if (!children.isEmpty()) {
+ double ownTime = getOwnTime();
+ msg += " " + ownTime + " ms spent in own code";
+ if (getCount() > 1) {
+ msg += " ("
+ + roundToSignificantFigures(ownTime / getCount())
+ + " ms per time)";
+ }
+ msg += '.';
+ }
+ return msg;
+ }
+
+ private static double roundToSignificantFigures(double num) {
+ // Number of significant digits
+ int n = 3;
+ if (num == 0) {
+ return 0;
+ }
+
+ final double d = Math.ceil(Math.log10(num < 0 ? -num : num));
+ final int power = n - (int) d;
+
+ final double magnitude = Math.pow(10, power);
+ final long shifted = Math.round(num * magnitude);
+ return shifted / magnitude;
+ }
+
+ public void sumUpTotals(Map<String, Node> totals) {
+ String name = getName();
+ if (name != null) {
+ Node totalNode = totals.get(name);
+ if (totalNode == null) {
+ totalNode = new Node(name);
+ totals.put(name, totalNode);
+ }
+
+ totalNode.time += getOwnTime();
+ totalNode.count += getCount();
+ totalNode.minTime = Math.min(totalNode.minTime,
+ getMinTimeSpent());
+ totalNode.maxTime = Math.max(totalNode.maxTime,
+ getMaxTimeSpent());
+ }
+ for (Node node : children.values()) {
+ node.sumUpTotals(totals);
+ }
+ }
+
+ /**
+ * @param timestamp
+ */
+ public void leave(double timestamp) {
+ double elapsed = (timestamp - enterTime);
+ time += elapsed;
+ enterTime = 0;
+ if (elapsed < minTime) {
+ minTime = elapsed;
+ }
+ if (elapsed > maxTime) {
+ maxTime = elapsed;
+ }
+ }
+ }
+
private static final String evtGroup = "VaadinProfiler";
private static final class GwtStatsEvent extends JavaScriptObject {
diff --git a/client/src/com/vaadin/client/RenderSpace.java b/client/src/com/vaadin/client/RenderSpace.java
index 5a7440b682..dff774aa6f 100644
--- a/client/src/com/vaadin/client/RenderSpace.java
+++ b/client/src/com/vaadin/client/RenderSpace.java
@@ -34,7 +34,7 @@ public class RenderSpace extends Size {
public RenderSpace(int width, int height, boolean useNativeScrollbarSize) {
super(width, height);
if (useNativeScrollbarSize) {
- scrollBarSize = Util.getNativeScrollbarSize();
+ scrollBarSize = WidgetUtil.getNativeScrollbarSize();
}
}
diff --git a/client/src/com/vaadin/client/ResourceLoader.java b/client/src/com/vaadin/client/ResourceLoader.java
index ceede263fc..9e9ce5ac49 100644
--- a/client/src/com/vaadin/client/ResourceLoader.java
+++ b/client/src/com/vaadin/client/ResourceLoader.java
@@ -225,7 +225,7 @@ public class ResourceLoader {
*/
public void loadScript(final String scriptUrl,
final ResourceLoadListener resourceLoadListener, boolean async) {
- final String url = Util.getAbsoluteUrl(scriptUrl);
+ final String url = WidgetUtil.getAbsoluteUrl(scriptUrl);
ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
if (loadedResources.contains(url)) {
if (resourceLoadListener != null) {
@@ -307,7 +307,7 @@ public class ResourceLoader {
*/
public void preloadResource(String url,
ResourceLoadListener resourceLoadListener) {
- url = Util.getAbsoluteUrl(url);
+ url = WidgetUtil.getAbsoluteUrl(url);
ResourceLoadEvent event = new ResourceLoadEvent(this, url, true);
if (loadedResources.contains(url) || preloadedResources.contains(url)) {
// Already loaded or preloaded -> just fire listener
@@ -424,7 +424,7 @@ public class ResourceLoader {
*/
public void loadStylesheet(final String stylesheetUrl,
final ResourceLoadListener resourceLoadListener) {
- final String url = Util.getAbsoluteUrl(stylesheetUrl);
+ final String url = WidgetUtil.getAbsoluteUrl(stylesheetUrl);
final ResourceLoadEvent event = new ResourceLoadEvent(this, url, false);
if (loadedResources.contains(url)) {
if (resourceLoadListener != null) {
diff --git a/client/src/com/vaadin/client/StyleConstants.java b/client/src/com/vaadin/client/StyleConstants.java
index c4588587d4..fad88f1359 100644
--- a/client/src/com/vaadin/client/StyleConstants.java
+++ b/client/src/com/vaadin/client/StyleConstants.java
@@ -35,4 +35,13 @@ public class StyleConstants {
* Added to all layouts to denote they are layouts
*/
public static final String UI_LAYOUT = "v-layout";
+
+ public static final String MODIFIED = "v-modified";
+ public static final String DISABLED = "v-disabled";
+
+ public static final String REQUIRED = "v-required";
+
+ public static final String REQUIRED_EXT = "-required";
+
+ public static final String ERROR_EXT = "-error";
}
diff --git a/client/src/com/vaadin/client/SuperDevMode.java b/client/src/com/vaadin/client/SuperDevMode.java
index f1020b3d25..821af6075a 100644
--- a/client/src/com/vaadin/client/SuperDevMode.java
+++ b/client/src/com/vaadin/client/SuperDevMode.java
@@ -89,7 +89,7 @@ public class SuperDevMode {
VConsole.error("JSONP compile call failed");
// Don't log exception as they are shown as
// notifications
- VConsole.error(Util.getSimpleName(caught) + ": "
+ VConsole.error(caught.getClass().getSimpleName() + ": "
+ caught.getMessage());
failed();
diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java
index 585045ddd5..778f7c3861 100644
--- a/client/src/com/vaadin/client/Util.java
+++ b/client/src/com/vaadin/client/Util.java
@@ -16,38 +16,20 @@
package com.vaadin.client;
-import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-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.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.AnchorElement;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.Document;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
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.Style.Display;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.KeyEvent;
-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.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.client.RenderInformation.FloatSize;
import com.vaadin.client.ui.VOverlay;
@@ -57,6 +39,9 @@ import com.vaadin.shared.communication.MethodInvocation;
import com.vaadin.shared.ui.ComponentStateUtil;
import com.vaadin.shared.util.SharedUtil;
+import elemental.js.json.JsJsonValue;
+import elemental.json.JsonValue;
+
public class Util {
/**
@@ -65,11 +50,10 @@ public class Util {
* Stops execution on firefox browsers on a breakpoint.
*
*/
- public static native void browserDebugger()
- /*-{
- if($wnd.console)
- debugger;
- }-*/;
+ @Deprecated
+ public static void browserDebugger() {
+ WidgetUtil.browserDebugger();
+ }
/**
* Helper method for a bug fix #14041. For mozilla getKeyCode return 0 for
@@ -80,12 +64,9 @@ public class Util {
* @return return key code
* @since 7.2.4
*/
+ @Deprecated
public static int getKeyCode(KeyEvent<?> event) {
- int keyCode = event.getNativeEvent().getKeyCode();
- if (keyCode == 0) {
- keyCode = event.getNativeEvent().getCharCode();
- }
- return keyCode;
+ return WidgetUtil.getKeyCode(event);
}
/**
@@ -99,17 +80,11 @@ public class Util {
* @param y
* @return the element at given coordinates
*/
- public static native com.google.gwt.user.client.Element getElementFromPoint(
- int clientX, int clientY)
- /*-{
- var el = $wnd.document.elementFromPoint(clientX, clientY);
- // Call elementFromPoint two times to make sure IE8 also returns something sensible if the application is running in an iframe
- el = $wnd.document.elementFromPoint(clientX, clientY);
- if(el != null && el.nodeType == 3) {
- el = el.parentNode;
- }
- return el;
- }-*/;
+ @Deprecated
+ public static com.google.gwt.user.client.Element getElementFromPoint(
+ int clientX, int clientY) {
+ return DOM.asOld(WidgetUtil.getElementFromPoint(clientX, clientY));
+ }
/**
* This helper method can be called if components size have been changed
@@ -159,37 +134,20 @@ public class Util {
return null;
}
+ @Deprecated
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;
- }
+ return WidgetUtil.parseRelativeSize(size);
}
- private static final Element escapeHtmlHelper = DOM.createDiv();
-
/**
* Converts html entities to text.
*
* @param html
* @return escaped string presentation of given html
*/
+ @Deprecated
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;
+ return WidgetUtil.escapeHTML(html);
}
/**
@@ -199,16 +157,9 @@ public class Util {
* The string to escape
* @return An escaped version of <literal>attribute</literal>.
*/
+ @Deprecated
public static String escapeAttribute(String attribute) {
- if (attribute == null) {
- return "";
- }
- attribute = attribute.replace("\"", "&quot;");
- attribute = attribute.replace("'", "&#39;");
- attribute = attribute.replace(">", "&gt;");
- attribute = attribute.replace("<", "&lt;");
- attribute = attribute.replace("&", "&amp;");
- return attribute;
+ return WidgetUtil.escapeAttribute(attribute);
}
/**
@@ -221,222 +172,74 @@ public class Util {
* clone child tree also
* @return
*/
- public static native com.google.gwt.user.client.Element cloneNode(
- Element element, boolean deep)
- /*-{
- return element.cloneNode(deep);
- }-*/;
+ @Deprecated
+ public static com.google.gwt.user.client.Element cloneNode(Element element,
+ boolean deep) {
+ return DOM.asOld(WidgetUtil.cloneNode(element, deep));
+ }
+ @Deprecated
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;
- }
- element.getStyle().setWidth(widthGuess, Unit.PX);
- int padding = element.getOffsetWidth() - widthGuess;
-
- element.getStyle().setProperty("width", originalWidth);
-
- return padding;
+ return WidgetUtil.measureHorizontalPaddingAndBorder(element,
+ paddingGuess);
}
+ @Deprecated
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;
- }
- element.getStyle().setHeight(widthGuess, Unit.PX);
- int padding = element.getOffsetHeight() - widthGuess;
-
- element.getStyle().setProperty("height", originalHeight);
- return padding;
+ return WidgetUtil
+ .measureVerticalPaddingAndBorder(element, paddingGuess);
}
+ @Deprecated
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;
+ return WidgetUtil.measureHorizontalBorder(element);
}
+ @Deprecated
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;
+ return WidgetUtil.measureVerticalBorder(element);
}
+ @Deprecated
public static int measureMarginLeft(Element element) {
- return element.getAbsoluteLeft()
- - element.getParentElement().getAbsoluteLeft();
+ return WidgetUtil.measureMarginLeft(element);
}
+ @Deprecated
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) {
- widget.getElement().getStyle().setProperty("width", width);
- }
-
- private static void setHeight(Widget widget, String height) {
- widget.getElement().getStyle().setProperty("height", height);
+ return WidgetUtil.setHeightExcludingPaddingAndBorder(widget, height,
+ paddingBorderGuess);
}
+ @Deprecated
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);
- }
+ return WidgetUtil.setWidthExcludingPaddingAndBorder(widget, width,
+ paddingBorderGuess);
}
+ @Deprecated
public static int setWidthExcludingPaddingAndBorder(Element element,
int requestedWidth, int horizontalPaddingBorderGuess,
boolean requestedWidthIncludesPaddingBorder) {
-
- int widthGuess = requestedWidth - horizontalPaddingBorderGuess;
- if (widthGuess < 0) {
- widthGuess = 0;
- }
-
- element.getStyle().setWidth(widthGuess, Unit.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;
- }
- element.getStyle().setWidth(w, Unit.PX);
-
- }
-
- return actualPadding;
-
+ return WidgetUtil.setWidthExcludingPaddingAndBorder(element,
+ requestedWidth, horizontalPaddingBorderGuess,
+ requestedWidthIncludesPaddingBorder);
}
+ @Deprecated
public static int setHeightExcludingPaddingAndBorder(Element element,
int requestedHeight, int verticalPaddingBorderGuess,
boolean requestedHeightIncludesPaddingBorder) {
-
- int heightGuess = requestedHeight - verticalPaddingBorderGuess;
- if (heightGuess < 0) {
- heightGuess = 0;
- }
-
- element.getStyle().setHeight(heightGuess, Unit.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;
- }
- element.getStyle().setHeight(h, Unit.PX);
-
- }
-
- return actualPadding;
-
+ return WidgetUtil.setHeightExcludingPaddingAndBorder(element,
+ requestedHeight, verticalPaddingBorderGuess,
+ requestedHeightIncludesPaddingBorder);
}
+ @Deprecated
public static String getSimpleName(Object widget) {
if (widget == null) {
return "(null)";
@@ -446,31 +249,14 @@ public class Util {
return name.substring(name.lastIndexOf('.') + 1);
}
+ @Deprecated
public static void setFloat(Element element, String value) {
- if (BrowserInfo.get().isIE()) {
- element.getStyle().setProperty("styleFloat", value);
- } else {
- element.getStyle().setProperty("cssFloat", value);
- }
+ WidgetUtil.setFloat(element, value);
}
- private static int detectedScrollbarSize = -1;
-
+ @Deprecated
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;
+ return WidgetUtil.getNativeScrollbarSize();
}
/**
@@ -480,15 +266,9 @@ public class Util {
* @param elem
* with overflow auto
*/
+ @Deprecated
public static void runWebkitOverflowAutoFixDeferred(final Element elem) {
- Scheduler.get().scheduleDeferred(new Command() {
-
- @Override
- public void execute() {
- Util.runWebkitOverflowAutoFix(elem);
- }
- });
-
+ WidgetUtil.runWebkitOverflowAutoFixDeferred(elem);
}
/**
@@ -499,66 +279,9 @@ public class Util {
* @param elem
* with overflow auto
*/
+ @Deprecated
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);
- }
- }
- });
- }
-
+ WidgetUtil.runWebkitOverflowAutoFix(elem);
}
/**
@@ -576,8 +299,8 @@ public class Util {
return null;
}
- float relativeWidth = Util.parseRelativeSize(state.width);
- float relativeHeight = Util.parseRelativeSize(state.height);
+ float relativeWidth = WidgetUtil.parseRelativeSize(state.width);
+ float relativeHeight = WidgetUtil.parseRelativeSize(state.height);
FloatSize relativeSize = new FloatSize(relativeWidth, relativeHeight);
return relativeSize;
@@ -589,10 +312,9 @@ public class Util {
return uidl.getBooleanAttribute("cached");
}
+ @Deprecated
public static void alert(String string) {
- if (true) {
- Window.alert(string);
- }
+ WidgetUtil.alert(string);
}
/**
@@ -625,21 +347,9 @@ public class Util {
* The element to check
* @return The border-box width for the element
*/
+ @Deprecated
public static int getRequiredWidth(com.google.gwt.dom.client.Element element) {
- int reqWidth = getRequiredWidthBoundingClientRect(element);
- if (BrowserInfo.get().isIE() && !BrowserInfo.get().isIE8()) {
- int csSize = getRequiredWidthComputedStyle(element);
- if (csSize == reqWidth + 1) {
- // If computed style reports one pixel larger than requiredWidth
- // we would be rounding in the wrong direction in IE9. Round up
- // instead.
- // We do not always use csSize as it e.g. for 100% wide Labels
- // in GridLayouts produces senseless values (see e.g.
- // ThemeTestUI with Runo).
- return csSize;
- }
- }
- return reqWidth;
+ return WidgetUtil.getRequiredWidth(element);
}
/**
@@ -650,94 +360,44 @@ public class Util {
* The element to check
* @return The border-box height for the element
*/
+ @Deprecated
public static int getRequiredHeight(
com.google.gwt.dom.client.Element element) {
- int reqHeight = getRequiredHeightBoundingClientRect(element);
- if (BrowserInfo.get().isIE() && !BrowserInfo.get().isIE8()) {
- int csSize = getRequiredHeightComputedStyle(element);
- if (csSize == reqHeight + 1) {
- // If computed style reports one pixel larger than
- // requiredHeight we would be rounding in the wrong direction in
- // IE9. Round up instead.
- // We do not always use csSize as it e.g. for 100% wide Labels
- // in GridLayouts produces senseless values (see e.g.
- // ThemeTestUI with Runo).
- return csSize;
- }
- }
- return reqHeight;
+ return WidgetUtil.getRequiredHeight(element);
}
- public static native int getRequiredWidthBoundingClientRect(
- 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;
- }
- }-*/;
+ @Deprecated
+ public int getRequiredWidthBoundingClientRect(
+ com.google.gwt.dom.client.Element element) {
+ return WidgetUtil.getRequiredWidthBoundingClientRect(element);
+ }
- public static native int getRequiredHeightComputedStyle(
- com.google.gwt.dom.client.Element element)
- /*-{
- var cs = element.ownerDocument.defaultView.getComputedStyle(element);
- var heightPx = cs.height;
- if(heightPx == 'auto'){
- // Fallback for when IE reports auto
- heightPx = @com.vaadin.client.Util::getRequiredHeightBoundingClientRect(Lcom/google/gwt/dom/client/Element;)(element) + 'px';
- }
- var borderTopPx = cs.borderTop;
- var borderBottomPx = cs.borderBottom;
- var paddingTopPx = cs.paddingTop;
- var paddingBottomPx = cs.paddingBottom;
-
- var height = heightPx.substring(0,heightPx.length-2);
- var border = borderTopPx.substring(0,borderTopPx.length-2)+borderBottomPx.substring(0,borderBottomPx.length-2);
- var padding = paddingTopPx.substring(0,paddingTopPx.length-2)+paddingBottomPx.substring(0,paddingBottomPx.length-2);
- return Math.ceil(height+border+padding);
- }-*/;
-
- public static native int getRequiredWidthComputedStyle(
- com.google.gwt.dom.client.Element element)
- /*-{
- var cs = element.ownerDocument.defaultView.getComputedStyle(element);
- var widthPx = cs.width;
- if(widthPx == 'auto'){
- // Fallback for when IE reports auto
- widthPx = @com.vaadin.client.Util::getRequiredWidthBoundingClientRect(Lcom/google/gwt/dom/client/Element;)(element) + 'px';
- }
- var borderLeftPx = cs.borderLeft;
- var borderRightPx = cs.borderRight;
- var paddingLeftPx = cs.paddingLeft;
- var paddingRightPx = cs.paddingRight;
-
- var width = widthPx.substring(0,widthPx.length-2);
- var border = borderLeftPx.substring(0,borderLeftPx.length-2)+borderRightPx.substring(0,borderRightPx.length-2);
- var padding = paddingLeftPx.substring(0,paddingLeftPx.length-2)+paddingRightPx.substring(0,paddingRightPx.length-2);
- return Math.ceil(width+border+padding);
- }-*/;
-
- public static native int getRequiredHeightBoundingClientRect(
- 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;
- }-*/;
+ @Deprecated
+ public static int getRequiredHeightComputedStyle(
+ com.google.gwt.dom.client.Element element) {
+ return WidgetUtil.getRequiredHeightComputedStyle(element);
+ }
+
+ @Deprecated
+ public static int getRequiredWidthComputedStyle(
+ com.google.gwt.dom.client.Element element) {
+ return WidgetUtil.getRequiredWidthComputedStyle(element);
+ }
+
+ @Deprecated
+ public static int getRequiredHeightBoundingClientRect(
+ com.google.gwt.dom.client.Element element) {
+ return WidgetUtil.getRequiredHeightBoundingClientRect(element);
+ }
+ @Deprecated
public static int getRequiredWidth(Widget widget) {
- return getRequiredWidth(widget.getElement());
+ return WidgetUtil.getRequiredWidth(widget);
}
+ @Deprecated
public static int getRequiredHeight(Widget widget) {
- return getRequiredHeight(widget.getElement());
+ return WidgetUtil.getRequiredHeight(widget);
}
/**
@@ -747,53 +407,12 @@ public class Util {
* the element to detect
* @return true if auto or scroll
*/
+ @Deprecated
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;
- }
+ return WidgetUtil.mayHaveScrollBars(pe);
}
/**
- * 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
@@ -863,14 +482,10 @@ public class Util {
* @param el
* the element to focus
*/
- public static native void focus(Element el)
- /*-{
- try {
- el.focus();
- } catch (e) {
-
- }
- }-*/;
+ @Deprecated
+ public static void focus(Element el) {
+ WidgetUtil.focus(el);
+ }
/**
* Helper method to find the nearest parent paintable instance by traversing
@@ -899,33 +514,10 @@ public class Util {
* @param class1
* the Widget type to seek for
*/
- @SuppressWarnings("unchecked")
+ @Deprecated
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.getParentElement();
- }
- }
- if (eventListener instanceof Widget) {
- /*
- * 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;
+ return WidgetUtil.findWidget(element, class1);
}
/**
@@ -934,14 +526,9 @@ public class Util {
* @param element
* The element that should be redrawn
*/
+ @Deprecated
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", "");
- }
+ WidgetUtil.forceWebkitRedraw(element);
}
/**
@@ -952,10 +539,9 @@ public class Util {
* @param e
* The element to perform the hack on
*/
+ @Deprecated
public static final void forceIE8Redraw(Element e) {
- if (BrowserInfo.get().isIE8()) {
- forceIERedraw(e);
- }
+ WidgetUtil.forceIE8Redraw(e);
}
/**
@@ -967,10 +553,9 @@ public class Util {
* @param e
* The element to perform the hack on
*/
+ @Deprecated
public static void forceIERedraw(Element e) {
- if (BrowserInfo.get().isIE()) {
- setStyleTemporarily(e, "zoom", "1");
- }
+ WidgetUtil.forceIERedraw(e);
}
/**
@@ -982,33 +567,14 @@ public class Util {
* @param element
* The element to detach and re-attach
*/
+ @Deprecated
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);
- }
-
+ WidgetUtil.detachAttach(element);
}
+ @Deprecated
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(imgElements.getItem(i), Event.ONLOAD);
- }
-
+ WidgetUtil.sinkOnloadForImages(element);
}
/**
@@ -1017,14 +583,9 @@ public class Util {
* @param subElement
* @return
*/
+ @Deprecated
public static int getChildElementIndex(Element childElement) {
- int idx = 0;
- Node n = childElement;
- while ((n = n.getPreviousSibling()) != null) {
- idx++;
- }
-
- return idx;
+ return WidgetUtil.getChildElementIndex(childElement);
}
private static void printConnectorInvocations(
@@ -1097,15 +658,10 @@ public class Util {
* @param tempValue
* The temporary value
*/
+ @Deprecated
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);
-
+ WidgetUtil.setStyleTemporarily(element, styleProperty, tempValue);
}
/**
@@ -1116,12 +672,9 @@ public class Util {
* @param event
* @return
*/
+ @Deprecated
public static int getTouchOrMouseClientX(Event event) {
- if (isTouchEvent(event)) {
- return event.getChangedTouches().get(0).getClientX();
- } else {
- return event.getClientX();
- }
+ return WidgetUtil.getTouchOrMouseClientX(event);
}
/**
@@ -1133,12 +686,10 @@ public class Util {
* the mouse event to get coordinates from
* @return the element at the coordinates of the event
*/
+ @Deprecated
public static com.google.gwt.user.client.Element getElementUnderMouse(
NativeEvent event) {
- int pageX = getTouchOrMouseClientX(event);
- int pageY = getTouchOrMouseClientY(event);
-
- return getElementFromPoint(pageX, pageY);
+ return DOM.asOld(WidgetUtil.getElementUnderMouse(event));
}
/**
@@ -1149,12 +700,9 @@ public class Util {
* @param event
* @return
*/
+ @Deprecated
public static int getTouchOrMouseClientY(Event event) {
- if (isTouchEvent(event)) {
- return event.getChangedTouches().get(0).getClientY();
- } else {
- return event.getClientY();
- }
+ return WidgetUtil.getTouchOrMouseClientY(event);
}
/**
@@ -1163,8 +711,9 @@ public class Util {
* @param currentGwtEvent
* @return
*/
+ @Deprecated
public static int getTouchOrMouseClientY(NativeEvent currentGwtEvent) {
- return getTouchOrMouseClientY(Event.as(currentGwtEvent));
+ return WidgetUtil.getTouchOrMouseClientY(currentGwtEvent);
}
/**
@@ -1173,67 +722,25 @@ public class Util {
* @param event
* @return
*/
+ @Deprecated
public static int getTouchOrMouseClientX(NativeEvent event) {
- return getTouchOrMouseClientX(Event.as(event));
+ return WidgetUtil.getTouchOrMouseClientX(event);
}
+ @Deprecated
public static boolean isTouchEvent(Event event) {
- return event.getType().contains("touch");
+ return WidgetUtil.isTouchEvent(event);
}
+ @Deprecated
public static boolean isTouchEvent(NativeEvent event) {
- return isTouchEvent(Event.as(event));
+ return WidgetUtil.isTouchEvent(event);
}
+ @Deprecated
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) {
- }
-
- }
- });
-
+ WidgetUtil.simulateClickFromTouchEvent(touchevent, widget);
}
/**
@@ -1241,14 +748,10 @@ public class Util {
*
* @return The active element or null if no active element could be found.
*/
- public native static com.google.gwt.user.client.Element getFocusedElement()
- /*-{
- if ($wnd.document.activeElement) {
- return $wnd.document.activeElement;
- }
-
- return null;
- }-*/;
+ @Deprecated
+ public static com.google.gwt.user.client.Element getFocusedElement() {
+ return DOM.asOld(WidgetUtil.getFocusedElement());
+ }
/**
* Gets the currently focused element for Internet Explorer.
@@ -1268,18 +771,9 @@ public class Util {
*
* @return true if focused element is editable
*/
+ @Deprecated
public static boolean isFocusedElementEditable() {
- Element focusedElement = Util.getFocusedElement();
- if (focusedElement != null) {
- String tagName = focusedElement.getTagName();
- String contenteditable = focusedElement
- .getAttribute("contenteditable");
-
- return "textarea".equalsIgnoreCase(tagName)
- || "input".equalsIgnoreCase(tagName)
- || "true".equalsIgnoreCase(contenteditable);
- }
- return false;
+ return WidgetUtil.isFocusedElementEditable();
}
/**
@@ -1291,30 +785,9 @@ public class Util {
* @param widget
* @return true if attached and displayed
*/
+ @Deprecated
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;
- }
+ return WidgetUtil.isAttachedAndDisplayed(widget);
}
/**
@@ -1324,33 +797,10 @@ public class Util {
* @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;
- }
- }-*/;
+ @Deprecated
+ public static void scrollIntoViewVertically(Element elem) {
+ WidgetUtil.scrollIntoViewVertically(elem);
+ }
/**
* Checks if the given event is either a touch event or caused by the left
@@ -1360,9 +810,9 @@ public class Util {
* @return true if the event is a touch event or caused by the left mouse
* button, false otherwise
*/
+ @Deprecated
public static boolean isTouchEventOrLeftMouseButton(Event event) {
- boolean touchEvent = Util.isTouchEvent(event);
- return touchEvent || event.getButton() == Event.BUTTON_LEFT;
+ return WidgetUtil.isTouchEventOrLeftMouseButton(event);
}
/**
@@ -1418,26 +868,9 @@ public class Util {
* a string with the relative URL to resolve
* @return the corresponding absolute URL as a string
*/
+ @Deprecated
public static String getAbsoluteUrl(String url) {
- if (BrowserInfo.get().isIE8()) {
- // The hard way - must use innerHTML and attach to DOM in IE8
- DivElement divElement = Document.get().createDivElement();
- divElement.getStyle().setDisplay(Display.NONE);
-
- RootPanel.getBodyElement().appendChild(divElement);
- divElement.setInnerHTML("<a href='" + escapeAttribute(url)
- + "' ></a>");
-
- AnchorElement a = divElement.getChild(0).cast();
- String href = a.getHref();
-
- RootPanel.getBodyElement().removeChild(divElement);
- return href;
- } else {
- AnchorElement a = Document.get().createAnchorElement();
- a.setHref(url);
- return a.getHref();
- }
+ return WidgetUtil.getAbsoluteUrl(url);
}
/**
@@ -1461,169 +894,70 @@ public class Util {
*
* @since 7.3
*/
- public native static void setSelectionRange(Element elem, int pos,
- int length, String direction)
- /*-{
- try {
- elem.setSelectionRange(pos, pos + length, direction);
- } catch (e) {
- // Firefox throws exception if TextBox is not visible, even if attached
- }
- }-*/;
+ @Deprecated
+ public static void setSelectionRange(Element elem, int pos, int length,
+ String direction) {
+ WidgetUtil.setSelectionRange(elem, pos, length, direction);
+ }
/**
- * Wrap a css size value and its unit and translate back and forth to the
- * string representation.<br/>
- * Eg. 50%, 123px, ...
- *
- * @since 7.2.6
- * @author Vaadin Ltd
+ * Converts a native {@link JavaScriptObject} into a {@link JsonValue}. This
+ * is a no-op in GWT code compiled to javascript, but needs some special
+ * handling to work when run in JVM.
+ *
+ * @param jso
+ * the java script object to represent as json
+ * @return the json representation
*/
- @SuppressWarnings("serial")
- public static class CssSize implements Serializable {
-
- /*
- * Map the size units with their type.
- */
- private static Map<String, Unit> type2Unit = new HashMap<String, Style.Unit>();
- static {
- for (Unit unit : Unit.values()) {
- type2Unit.put(unit.getType(), unit);
- }
- }
-
- /**
- * Gets the unit value by its type.
- *
- * @param type
- * the type of the unit as found in the style.
- * @return the unit value.
- */
- public static Unit unitByType(String type) {
- return type2Unit.get(type);
- }
-
- /*
- * Regex to parse the size.
- */
- private static final RegExp sizePattern = RegExp
- .compile(SharedUtil.SIZE_PATTERN);
-
- /**
- * Parse the size from string format to {@link CssSize}.
- *
- * @param s
- * the size as string.
- * @return a {@link CssSize} object.
- */
- public static CssSize fromString(String s) {
- if (s == null) {
- return null;
- }
-
- s = s.trim();
- if ("".equals(s)) {
- return null;
- }
-
- float size = 0;
- Unit unit = null;
-
- MatchResult matcher = sizePattern.exec(s);
- if (matcher.getGroupCount() > 1) {
-
- size = Float.parseFloat(matcher.getGroup(1));
- if (size < 0) {
- size = -1;
- unit = Unit.PX;
-
- } else {
- String symbol = matcher.getGroup(2);
- unit = unitByType(symbol);
- }
- } else {
- throw new IllegalArgumentException("Invalid size argument: \""
- + s + "\" (should match " + sizePattern.getSource()
- + ")");
- }
- return new CssSize(size, unit);
- }
-
- /**
- * Creates a {@link CssSize} using a value and its measurement unit.
- *
- * @param value
- * the value.
- * @param unit
- * the unit.
- * @return the {@link CssSize} object.
- */
- public static CssSize fromValueUnit(float value, Unit unit) {
- return new CssSize(value, unit);
- }
-
- /*
- * The value.
- */
- private final float value;
-
- /*
- * The measure unit.
- */
- private final Unit unit;
-
- private CssSize(float value, Unit unit) {
- this.value = value;
- this.unit = unit;
- }
-
- /**
- * Gets the value for this css size.
- *
- * @return the value.
- */
- public float getValue() {
- return value;
- }
-
- /**
- * Gets the measurement unit for this css size.
- *
- * @return the unit.
- */
- public Unit getUnit() {
- return unit;
- }
-
- @Override
- public String toString() {
- return value + unit.getType();
+ public static <T extends JsonValue> T jso2json(JavaScriptObject jso) {
+ if (GWT.isProdMode()) {
+ return (T) jso.<JsJsonValue> cast();
+ } else {
+ return elemental.json.Json.instance().parse(stringify(jso));
}
+ }
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof CssSize) {
- CssSize size = (CssSize) obj;
- return size.value == value && size.unit == unit;
- }
-
- return false;
+ /**
+ * Converts a {@link JsonValue} into a native {@link JavaScriptObject}. This
+ * is a no-op in GWT code compiled to javascript, but needs some special
+ * handling to work when run in JVM.
+ *
+ * @param jsonValue
+ * the json value
+ * @return a native javascript object representation of the json value
+ */
+ public static JavaScriptObject json2jso(JsonValue jsonValue) {
+ if (GWT.isProdMode()) {
+ return ((JavaScriptObject) jsonValue.toNative()).cast();
+ } else {
+ return parse(jsonValue.toJson());
}
+ }
- /**
- * Check whether the two sizes are equals.
- *
- * @param cssSize1
- * the first size to compare.
- * @param cssSize2
- * the other size to compare with the first one.
- * @return true if the two sizes are equals, otherwise false.
- */
- public static boolean equals(String cssSize1, String cssSize2) {
- return CssSize.fromString(cssSize1).equals(
- CssSize.fromString(cssSize2));
- }
+ /**
+ * Convert a {@link JavaScriptObject} into a string representation.
+ *
+ * @param json
+ * a JavaScript object to be converted to a string
+ * @return JSON in string representation
+ */
+ private native static String stringify(JavaScriptObject json)
+ /*-{
+ return JSON.stringify(json);
+ }-*/;
- }
+ /**
+ * Parse a string containing JSON into a {@link JavaScriptObject}.
+ *
+ * @param <T>
+ * the overlay type to expect from the parse
+ * @param jsonAsString
+ * @return a JavaScript object constructed from the parse
+ */
+ public native static <T extends JavaScriptObject> T parse(
+ String jsonAsString)
+ /*-{
+ return JSON.parse(jsonAsString);
+ }-*/;
}
diff --git a/client/src/com/vaadin/client/VCaption.java b/client/src/com/vaadin/client/VCaption.java
index eb19dedf8b..050edae8be 100644
--- a/client/src/com/vaadin/client/VCaption.java
+++ b/client/src/com/vaadin/client/VCaption.java
@@ -148,7 +148,7 @@ public class VCaption extends HTML {
}
}
if (!owner.isEnabled()) {
- style += " " + ApplicationConnection.DISABLED_CLASSNAME;
+ style += " " + StyleConstants.DISABLED;
}
setStyleName(style);
@@ -328,7 +328,7 @@ public class VCaption extends HTML {
String style = VCaption.CLASSNAME;
if (disabled) {
- style += " " + ApplicationConnection.DISABLED_CLASSNAME;
+ style += " " + StyleConstants.DISABLED;
}
setStyleName(style);
if (hasDescription) {
@@ -510,17 +510,17 @@ public class VCaption extends HTML {
int width = 0;
if (icon != null) {
- width += Util.getRequiredWidth(icon.getElement());
+ width += WidgetUtil.getRequiredWidth(icon.getElement());
}
if (captionText != null) {
- width += Util.getRequiredWidth(captionText);
+ width += WidgetUtil.getRequiredWidth(captionText);
}
if (requiredFieldIndicator != null) {
- width += Util.getRequiredWidth(requiredFieldIndicator);
+ width += WidgetUtil.getRequiredWidth(requiredFieldIndicator);
}
if (errorIndicatorElement != null) {
- width += Util.getRequiredWidth(errorIndicatorElement);
+ width += WidgetUtil.getRequiredWidth(errorIndicatorElement);
}
return width;
@@ -531,7 +531,7 @@ public class VCaption extends HTML {
int width = 0;
if (icon != null) {
- width += Util.getRequiredWidth(icon.getElement());
+ width += WidgetUtil.getRequiredWidth(icon.getElement());
}
if (captionText != null) {
int textWidth = captionText.getScrollWidth();
@@ -540,7 +540,7 @@ public class VCaption extends HTML {
* In Firefox3 the caption might require more space than the
* scrollWidth returns as scrollWidth is rounded down.
*/
- int requiredWidth = Util.getRequiredWidth(captionText);
+ int requiredWidth = WidgetUtil.getRequiredWidth(captionText);
if (requiredWidth > textWidth) {
textWidth = requiredWidth;
}
@@ -549,10 +549,10 @@ public class VCaption extends HTML {
width += textWidth;
}
if (requiredFieldIndicator != null) {
- width += Util.getRequiredWidth(requiredFieldIndicator);
+ width += WidgetUtil.getRequiredWidth(requiredFieldIndicator);
}
if (errorIndicatorElement != null) {
- width += Util.getRequiredWidth(errorIndicatorElement);
+ width += WidgetUtil.getRequiredWidth(errorIndicatorElement);
}
return width;
@@ -564,26 +564,26 @@ public class VCaption extends HTML {
int h;
if (icon != null) {
- h = Util.getRequiredHeight(icon.getElement());
+ h = WidgetUtil.getRequiredHeight(icon.getElement());
if (h > height) {
height = h;
}
}
if (captionText != null) {
- h = Util.getRequiredHeight(captionText);
+ h = WidgetUtil.getRequiredHeight(captionText);
if (h > height) {
height = h;
}
}
if (requiredFieldIndicator != null) {
- h = Util.getRequiredHeight(requiredFieldIndicator);
+ h = WidgetUtil.getRequiredHeight(requiredFieldIndicator);
if (h > height) {
height = h;
}
}
if (errorIndicatorElement != null) {
- h = Util.getRequiredHeight(errorIndicatorElement);
+ h = WidgetUtil.getRequiredHeight(errorIndicatorElement);
if (h > height) {
height = h;
}
@@ -619,11 +619,13 @@ public class VCaption extends HTML {
// DOM.setStyleAttribute(getElement(), "width", maxWidth + "px");
if (requiredFieldIndicator != null) {
- availableWidth -= Util.getRequiredWidth(requiredFieldIndicator);
+ availableWidth -= WidgetUtil
+ .getRequiredWidth(requiredFieldIndicator);
}
if (errorIndicatorElement != null) {
- availableWidth -= Util.getRequiredWidth(errorIndicatorElement);
+ availableWidth -= WidgetUtil
+ .getRequiredWidth(errorIndicatorElement);
}
if (availableWidth < 0) {
@@ -631,8 +633,8 @@ public class VCaption extends HTML {
}
if (icon != null) {
- int iconRequiredWidth = Util
- .getRequiredWidth(icon.getElement());
+ int iconRequiredWidth = WidgetUtil.getRequiredWidth(icon
+ .getElement());
if (availableWidth > iconRequiredWidth) {
availableWidth -= iconRequiredWidth;
} else {
@@ -642,7 +644,7 @@ public class VCaption extends HTML {
}
}
if (captionText != null) {
- int captionWidth = Util.getRequiredWidth(captionText);
+ int captionWidth = WidgetUtil.getRequiredWidth(captionText);
if (availableWidth > captionWidth) {
availableWidth -= captionWidth;
diff --git a/client/src/com/vaadin/client/VLoadingIndicator.java b/client/src/com/vaadin/client/VLoadingIndicator.java
index e873005d3a..7c7edeb04f 100644
--- a/client/src/com/vaadin/client/VLoadingIndicator.java
+++ b/client/src/com/vaadin/client/VLoadingIndicator.java
@@ -154,6 +154,18 @@ public class VLoadingIndicator {
}
/**
+ * Triggers displaying of this loading indicator unless it's already visible
+ * or scheduled to be shown after a delay.
+ *
+ * @since 7.4
+ */
+ public void ensureTriggered() {
+ if (!isVisible() && !firstTimer.isRunning()) {
+ trigger();
+ }
+ }
+
+ /**
* Shows the loading indicator in its standard state and triggers timers for
* transitioning into the "second" and "third" states.
*/
diff --git a/client/src/com/vaadin/client/VUIDLBrowser.java b/client/src/com/vaadin/client/VUIDLBrowser.java
index 4b4fd2f389..08f4c653a5 100644
--- a/client/src/com/vaadin/client/VUIDLBrowser.java
+++ b/client/src/com/vaadin/client/VUIDLBrowser.java
@@ -33,14 +33,16 @@ 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.client.ui.UnknownComponentConnector;
import com.vaadin.client.ui.VWindow;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import elemental.json.JsonType;
+import elemental.json.JsonValue;
+
/**
* @author Vaadin Ltd
*
@@ -159,7 +161,7 @@ public class VUIDLBrowser extends SimpleTree {
} else {
setText("Unknown connector (" + connectorId + ")");
}
- dir(new JSONObject(stateChanges), this);
+ dir((JsonObject) Util.jso2json(stateChanges), this);
}
@Override
@@ -167,28 +169,28 @@ public class VUIDLBrowser extends SimpleTree {
return connectorId;
}
- private void dir(String key, JSONValue value, SimpleTree tree) {
- if (value.isObject() != null) {
+ private void dir(String key, JsonValue value, SimpleTree tree) {
+ if (value.getType() == JsonType.OBJECT) {
SimpleTree subtree = new SimpleTree(key + "=object");
tree.add(subtree);
- dir(value.isObject(), subtree);
- } else if (value.isArray() != null) {
+ dir((JsonObject) value, subtree);
+ } else if (value.getType() == JsonType.ARRAY) {
SimpleTree subtree = new SimpleTree(key + "=array");
- dir(value.isArray(), subtree);
+ dir((JsonArray) value, subtree);
tree.add(subtree);
} else {
tree.addItem(key + "=" + value);
}
}
- private void dir(JSONObject state, SimpleTree tree) {
- for (String key : state.keySet()) {
+ private void dir(JsonObject state, SimpleTree tree) {
+ for (String key : state.keys()) {
dir(key, state.get(key), tree);
}
}
- private void dir(JSONArray array, SimpleTree tree) {
- for (int i = 0; i < array.size(); ++i) {
+ private void dir(JsonArray array, SimpleTree tree) {
+ for (int i = 0; i < array.length(); ++i) {
dir("" + i, array.get(i), tree);
}
}
diff --git a/client/src/com/vaadin/client/WidgetUtil.java b/client/src/com/vaadin/client/WidgetUtil.java
new file mode 100644
index 0000000000..96f161c4a8
--- /dev/null
+++ b/client/src/com/vaadin/client/WidgetUtil.java
@@ -0,0 +1,1405 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.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.Unit;
+import com.google.gwt.dom.client.Touch;
+import com.google.gwt.event.dom.client.KeyEvent;
+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.Event;
+import com.google.gwt.user.client.EventListener;
+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.vaadin.shared.util.SharedUtil;
+
+/**
+ * Utility methods which are related to client side code only
+ */
+public class WidgetUtil {
+
+ /**
+ * Helper method for debugging purposes.
+ *
+ * Stops execution on firefox browsers on a breakpoint.
+ *
+ */
+ public static native void browserDebugger()
+ /*-{
+ if($wnd.console)
+ debugger;
+ }-*/;
+
+ /**
+ * Helper method for a bug fix #14041. For mozilla getKeyCode return 0 for
+ * space bar (because space is considered as char). If return 0 use
+ * getCharCode.
+ *
+ * @param event
+ * @return return key code
+ * @since 7.2.4
+ */
+ public static int getKeyCode(KeyEvent<?> event) {
+ int keyCode = event.getNativeEvent().getKeyCode();
+ if (keyCode == 0) {
+ keyCode = event.getNativeEvent().getCharCode();
+ }
+ return keyCode;
+ }
+
+ /**
+ *
+ * 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);
+ // Call elementFromPoint two times to make sure IE8 also returns something sensible if the application is running in an iframe
+ el = $wnd.document.elementFromPoint(clientX, clientY);
+ if(el != null && el.nodeType == 3) {
+ el = el.parentNode;
+ }
+ return el;
+ }-*/;
+
+ 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) {
+ getLogger().warning("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) {
+ if (attribute == null) {
+ return "";
+ }
+ 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;
+ }
+ element.getStyle().setWidth(widthGuess, Unit.PX);
+ int padding = element.getOffsetWidth() - widthGuess;
+
+ element.getStyle().setProperty("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;
+ }
+ element.getStyle().setHeight(widthGuess, Unit.PX);
+ int padding = element.getOffsetHeight() - widthGuess;
+
+ element.getStyle().setProperty("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) {
+ widget.getElement().getStyle().setProperty("width", width);
+ }
+
+ private static void setHeight(Widget widget, String height) {
+ widget.getElement().getStyle().setProperty("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;
+ }
+
+ element.getStyle().setWidth(widthGuess, Unit.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;
+ }
+ element.getStyle().setWidth(w, Unit.PX);
+
+ }
+
+ return actualPadding;
+
+ }
+
+ public static int setHeightExcludingPaddingAndBorder(Element element,
+ int requestedHeight, int verticalPaddingBorderGuess,
+ boolean requestedHeightIncludesPaddingBorder) {
+
+ int heightGuess = requestedHeight - verticalPaddingBorderGuess;
+ if (heightGuess < 0) {
+ heightGuess = 0;
+ }
+
+ element.getStyle().setHeight(heightGuess, Unit.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;
+ }
+ element.getStyle().setHeight(h, Unit.PX);
+
+ }
+
+ return actualPadding;
+
+ }
+
+ public static void setFloat(Element element, String value) {
+ if (BrowserInfo.get().isIE()) {
+ element.getStyle().setProperty("styleFloat", value);
+ } else {
+ element.getStyle().setProperty("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;
+ }
+
+ /**
+ * Defers the execution of {@link #runWebkitOverflowAutoFix(Element)}
+ *
+ * @since 7.2.6
+ * @param elem
+ * with overflow auto
+ */
+ public static void runWebkitOverflowAutoFixDeferred(final Element elem) {
+ Scheduler.get().scheduleDeferred(new Command() {
+
+ @Override
+ public void execute() {
+ WidgetUtil.runWebkitOverflowAutoFix(elem);
+ }
+ });
+
+ }
+
+ /**
+ * 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);
+ }
+ }
+ });
+ }
+
+ }
+
+ public static void alert(String string) {
+ if (true) {
+ Window.alert(string);
+ }
+ }
+
+ /**
+ * Gets the border-box width for the given element, i.e. element width +
+ * border + padding. Always rounds up to nearest integer.
+ *
+ * @param element
+ * The element to check
+ * @return The border-box width for the element
+ */
+ public static int getRequiredWidth(com.google.gwt.dom.client.Element element) {
+ int reqWidth = getRequiredWidthBoundingClientRect(element);
+ if (BrowserInfo.get().isIE() && !BrowserInfo.get().isIE8()) {
+ int csSize = getRequiredWidthComputedStyle(element);
+ if (csSize == reqWidth + 1) {
+ // If computed style reports one pixel larger than requiredWidth
+ // we would be rounding in the wrong direction in IE9. Round up
+ // instead.
+ // We do not always use csSize as it e.g. for 100% wide Labels
+ // in GridLayouts produces senseless values (see e.g.
+ // ThemeTestUI with Runo).
+ return csSize;
+ }
+ }
+ return reqWidth;
+ }
+
+ /**
+ * Gets the border-box height for the given element, i.e. element height +
+ * border + padding. Always rounds up to nearest integer.
+ *
+ * @param element
+ * The element to check
+ * @return The border-box height for the element
+ */
+ public static int getRequiredHeight(
+ com.google.gwt.dom.client.Element element) {
+ int reqHeight = getRequiredHeightBoundingClientRect(element);
+ if (BrowserInfo.get().isIE() && !BrowserInfo.get().isIE8()) {
+ int csSize = getRequiredHeightComputedStyle(element);
+ if (csSize == reqHeight + 1) {
+ // If computed style reports one pixel larger than
+ // requiredHeight we would be rounding in the wrong direction in
+ // IE9. Round up instead.
+ // We do not always use csSize as it e.g. for 100% wide Labels
+ // in GridLayouts produces senseless values (see e.g.
+ // ThemeTestUI with Runo).
+ return csSize;
+ }
+ }
+ return reqHeight;
+ }
+
+ /**
+ * Calculates the width of the element's bounding rectangle.
+ * <p>
+ * In case the browser doesn't support bounding rectangles, the returned
+ * value is the offset width.
+ *
+ * @param element
+ * the element of which to calculate the width
+ * @return the width of the element
+ */
+ public static int getRequiredWidthBoundingClientRect(
+ com.google.gwt.dom.client.Element element) {
+ return (int) getRequiredWidthBoundingClientRectDouble(element);
+ }
+
+ /**
+ * Calculates the width of the element's bounding rectangle to subpixel
+ * precision.
+ * <p>
+ * In case the browser doesn't support bounding rectangles, the returned
+ * value is the offset width.
+ *
+ * @param element
+ * the element of which to calculate the width
+ * @return the subpixel-accurate width of the element
+ * @since 7.4
+ */
+ public static native double getRequiredWidthBoundingClientRectDouble(
+ 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 getRequiredHeightComputedStyle(
+ com.google.gwt.dom.client.Element element)
+ /*-{
+ var cs = element.ownerDocument.defaultView.getComputedStyle(element);
+ var heightPx = cs.height;
+ if(heightPx == 'auto'){
+ // Fallback for when IE reports auto
+ heightPx = @com.vaadin.client.WidgetUtil::getRequiredHeightBoundingClientRect(Lcom/google/gwt/dom/client/Element;)(element) + 'px';
+ }
+ var borderTopPx = cs.borderTop;
+ var borderBottomPx = cs.borderBottom;
+ var paddingTopPx = cs.paddingTop;
+ var paddingBottomPx = cs.paddingBottom;
+
+ var height = heightPx.substring(0,heightPx.length-2);
+ var border = borderTopPx.substring(0,borderTopPx.length-2)+borderBottomPx.substring(0,borderBottomPx.length-2);
+ var padding = paddingTopPx.substring(0,paddingTopPx.length-2)+paddingBottomPx.substring(0,paddingBottomPx.length-2);
+ return Math.ceil(height+border+padding);
+ }-*/;
+
+ public static native int getRequiredWidthComputedStyle(
+ com.google.gwt.dom.client.Element element)
+ /*-{
+ var cs = element.ownerDocument.defaultView.getComputedStyle(element);
+ var widthPx = cs.width;
+ if(widthPx == 'auto'){
+ // Fallback for when IE reports auto
+ widthPx = @com.vaadin.client.WidgetUtil::getRequiredWidthBoundingClientRect(Lcom/google/gwt/dom/client/Element;)(element) + 'px';
+ }
+ var borderLeftPx = cs.borderLeft;
+ var borderRightPx = cs.borderRight;
+ var paddingLeftPx = cs.paddingLeft;
+ var paddingRightPx = cs.paddingRight;
+
+ var width = widthPx.substring(0,widthPx.length-2);
+ var border = borderLeftPx.substring(0,borderLeftPx.length-2)+borderRightPx.substring(0,borderRightPx.length-2);
+ var padding = paddingLeftPx.substring(0,paddingLeftPx.length-2)+paddingRightPx.substring(0,paddingRightPx.length-2);
+ return Math.ceil(width+border+padding);
+ }-*/;
+
+ /**
+ * Calculates the height of the element's bounding rectangle.
+ * <p>
+ * In case the browser doesn't support bounding rectangles, the returned
+ * value is the offset height.
+ *
+ * @param element
+ * the element of which to calculate the height
+ * @return the height of the element
+ */
+ public static int getRequiredHeightBoundingClientRect(
+ com.google.gwt.dom.client.Element element) {
+ return (int) getRequiredHeightBoundingClientRectDouble(element);
+ }
+
+ /**
+ * Calculates the height of the element's bounding rectangle to subpixel
+ * precision.
+ * <p>
+ * In case the browser doesn't support bounding rectangles, the returned
+ * value is the offset height.
+ *
+ * @param element
+ * the element of which to calculate the height
+ * @return the subpixel-accurate height of the element
+ * @since 7.4
+ */
+ public static native double getRequiredHeightBoundingClientRectDouble(
+ 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 "";
+ }
+
+ }-*/;
+
+ /**
+ * 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 first instance of given Widget type found by
+ * traversing DOM upwards from given element.
+ * <p>
+ * <strong>Note:</strong> If {@code element} is inside some widget {@code W}
+ * , <em>and</em> {@code W} in turn is wrapped in a {@link Composite}
+ * {@code C}, this method will not find {@code W}. It returns either
+ * {@code C} or null, depending on whether the class parameter matches. This
+ * may also be the case with other Composite-like classes that hijack the
+ * event handling of their child widget(s).
+ *
+ * @param element
+ * the element where to start seeking of Widget
+ * @param class1
+ * the Widget type to seek for
+ */
+ @SuppressWarnings("unchecked")
+ 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.getParentElement();
+ }
+ }
+ if (eventListener instanceof Widget) {
+ /*
+ * 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", "");
+ }
+ }
+
+ /**
+ * Performs a hack to trigger a re-layout in the IE8. This is usually
+ * necessary in cases where IE8 "forgets" to update child elements when they
+ * resize.
+ *
+ * @param e
+ * The element to perform the hack on
+ */
+ public static final void forceIE8Redraw(Element e) {
+ if (BrowserInfo.get().isIE8()) {
+ forceIERedraw(e);
+ }
+ }
+
+ /**
+ * Performs a hack to trigger a re-layout in the IE browser. This is usually
+ * necessary in cases where IE "forgets" to update child elements when they
+ * resize.
+ *
+ * @since 7.3
+ * @param e
+ * The element to perform the hack on
+ */
+ public static void forceIERedraw(Element e) {
+ if (BrowserInfo.get().isIE()) {
+ setStyleTemporarily(e, "zoom", "1");
+ }
+ }
+
+ /**
+ * 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(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;
+ }
+
+ /**
+ * 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.
+ *
+ * @return The active element or null if no active element could be found.
+ */
+ public native static Element getFocusedElement()
+ /*-{
+ if ($wnd.document.activeElement) {
+ return $wnd.document.activeElement;
+ }
+
+ return null;
+ }-*/;
+
+ /**
+ * Gets currently focused element and checks if it's editable
+ *
+ * @since 7.4
+ *
+ * @return true if focused element is editable
+ */
+ public static boolean isFocusedElementEditable() {
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (focusedElement != null) {
+ String tagName = focusedElement.getTagName();
+ String contenteditable = focusedElement
+ .getAttribute("contenteditable");
+
+ return "textarea".equalsIgnoreCase(tagName)
+ || "input".equalsIgnoreCase(tagName)
+ || "true".equalsIgnoreCase(contenteditable);
+ }
+ return false;
+ }
+
+ /**
+ * 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 = WidgetUtil.isTouchEvent(event);
+ return touchEvent || event.getButton() == Event.BUTTON_LEFT;
+ }
+
+ /**
+ * Resolve a relative URL to an absolute URL based on the current document's
+ * location.
+ *
+ * @param url
+ * a string with the relative URL to resolve
+ * @return the corresponding absolute URL as a string
+ */
+ public static String getAbsoluteUrl(String url) {
+ if (BrowserInfo.get().isIE8()) {
+ // The hard way - must use innerHTML and attach to DOM in IE8
+ DivElement divElement = Document.get().createDivElement();
+ divElement.getStyle().setDisplay(Display.NONE);
+
+ RootPanel.getBodyElement().appendChild(divElement);
+ divElement.setInnerHTML("<a href='" + escapeAttribute(url)
+ + "' ></a>");
+
+ AnchorElement a = divElement.getChild(0).cast();
+ String href = a.getHref();
+
+ RootPanel.getBodyElement().removeChild(divElement);
+ return href;
+ } else {
+ AnchorElement a = Document.get().createAnchorElement();
+ a.setHref(url);
+ return a.getHref();
+ }
+ }
+
+ /**
+ * Sets the selection range of an input element.
+ *
+ * We need this JSNI function to set selection range so that we can use the
+ * optional direction attribute to set the anchor to the end and the focus
+ * to the start. This makes Firefox work the same way as other browsers
+ * (#13477)
+ *
+ * @param elem
+ * the html input element.
+ * @param pos
+ * the index of the first selected character.
+ * @param length
+ * the selection length.
+ * @param direction
+ * a string indicating the direction in which the selection was
+ * performed. This may be "forward" or "backward", or "none" if
+ * the direction is unknown or irrelevant.
+ *
+ * @since 7.3
+ */
+ public native static void setSelectionRange(Element elem, int pos,
+ int length, String direction)
+ /*-{
+ try {
+ elem.setSelectionRange(pos, pos + length, direction);
+ } catch (e) {
+ // Firefox throws exception if TextBox is not visible, even if attached
+ }
+ }-*/;
+
+ /**
+ * The allowed value inaccuracy when comparing two double-typed pixel
+ * values.
+ * <p>
+ * Since we're comparing pixels on a screen, epsilon must be less than 1.
+ * 0.49 was deemed a perfectly fine and beautifully round number.
+ */
+ public static final double PIXEL_EPSILON = 0.49d;
+
+ /**
+ * Compares two double values with the error margin of
+ * {@link #PIXEL_EPSILON} (i.e. {@value #PIXEL_EPSILON})
+ *
+ * @param num1
+ * the first value for which to compare equality
+ * @param num2
+ * the second value for which to compare equality
+ * @since 7.4
+ *
+ * @return true if the values are considered equals; false otherwise
+ */
+ public static boolean pixelValuesEqual(final double num1, final double num2) {
+ return Math.abs(num1 - num2) <= PIXEL_EPSILON;
+ }
+
+ /**
+ * Wrap a css size value and its unit and translate back and forth to the
+ * string representation.<br/>
+ * Eg. 50%, 123px, ...
+ *
+ * @since 7.2.6
+ * @author Vaadin Ltd
+ */
+ @SuppressWarnings("serial")
+ public static class CssSize implements Serializable {
+
+ /*
+ * Map the size units with their type.
+ */
+ private static Map<String, Unit> type2Unit = new HashMap<String, Style.Unit>();
+ static {
+ for (Unit unit : Unit.values()) {
+ type2Unit.put(unit.getType(), unit);
+ }
+ }
+
+ /**
+ * Gets the unit value by its type.
+ *
+ * @param type
+ * the type of the unit as found in the style.
+ * @return the unit value.
+ */
+ public static Unit unitByType(String type) {
+ return type2Unit.get(type);
+ }
+
+ /*
+ * Regex to parse the size.
+ */
+ private static final RegExp sizePattern = RegExp
+ .compile(SharedUtil.SIZE_PATTERN);
+
+ /**
+ * Parse the size from string format to {@link CssSize}.
+ *
+ * @param s
+ * the size as string.
+ * @return a {@link CssSize} object.
+ */
+ public static CssSize fromString(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ s = s.trim();
+ if ("".equals(s)) {
+ return null;
+ }
+
+ float size = 0;
+ Unit unit = null;
+
+ MatchResult matcher = sizePattern.exec(s);
+ if (matcher.getGroupCount() > 1) {
+
+ size = Float.parseFloat(matcher.getGroup(1));
+ if (size < 0) {
+ size = -1;
+ unit = Unit.PX;
+
+ } else {
+ String symbol = matcher.getGroup(2);
+ unit = unitByType(symbol);
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid size argument: \""
+ + s + "\" (should match " + sizePattern.getSource()
+ + ")");
+ }
+ return new CssSize(size, unit);
+ }
+
+ /**
+ * Creates a {@link CssSize} using a value and its measurement unit.
+ *
+ * @param value
+ * the value.
+ * @param unit
+ * the unit.
+ * @return the {@link CssSize} object.
+ */
+ public static CssSize fromValueUnit(float value, Unit unit) {
+ return new CssSize(value, unit);
+ }
+
+ /*
+ * The value.
+ */
+ private final float value;
+
+ /*
+ * The measure unit.
+ */
+ private final Unit unit;
+
+ private CssSize(float value, Unit unit) {
+ this.value = value;
+ this.unit = unit;
+ }
+
+ /**
+ * Gets the value for this css size.
+ *
+ * @return the value.
+ */
+ public float getValue() {
+ return value;
+ }
+
+ /**
+ * Gets the measurement unit for this css size.
+ *
+ * @return the unit.
+ */
+ public Unit getUnit() {
+ return unit;
+ }
+
+ @Override
+ public String toString() {
+ return value + unit.getType();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof CssSize) {
+ CssSize size = (CssSize) obj;
+ return size.value == value && size.unit == unit;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check whether the two sizes are equals.
+ *
+ * @param cssSize1
+ * the first size to compare.
+ * @param cssSize2
+ * the other size to compare with the first one.
+ * @return true if the two sizes are equals, otherwise false.
+ */
+ public static boolean equals(String cssSize1, String cssSize2) {
+ return CssSize.fromString(cssSize1).equals(
+ CssSize.fromString(cssSize2));
+ }
+
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(WidgetUtil.class.getName());
+ }
+
+}
diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java
index a2346db186..da08928f36 100644
--- a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java
+++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java
@@ -20,7 +20,6 @@ import java.util.ArrayList;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.json.client.JSONObject;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window.Location;
import com.vaadin.client.ApplicationConfiguration;
@@ -37,6 +36,8 @@ import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.shared.ui.ui.UIState.PushConfigurationState;
import com.vaadin.shared.util.SharedUtil;
+import elemental.json.JsonObject;
+
/**
* The default {@link PushConnection} implementation that uses Atmosphere for
* handling the communication channel.
@@ -114,7 +115,7 @@ public class AtmospherePushConnection implements PushConnection {
private JavaScriptObject socket;
- private ArrayList<JSONObject> messageQueue = new ArrayList<JSONObject>();
+ private ArrayList<JsonObject> messageQueue = new ArrayList<JsonObject>();
private State state = State.CONNECT_PENDING;
@@ -204,25 +205,25 @@ public class AtmospherePushConnection implements PushConnection {
}
@Override
- public void push(JSONObject message) {
+ public void push(JsonObject message) {
switch (state) {
case CONNECT_PENDING:
assert isActive();
- VConsole.log("Queuing push message: " + message);
+ VConsole.log("Queuing push message: " + message.toJson());
messageQueue.add(message);
break;
case CONNECTED:
assert isActive();
- VConsole.log("Sending push message: " + message);
+ VConsole.log("Sending push message: " + message.toJson());
if (transport.equals("websocket")) {
FragmentedMessage fragmented = new FragmentedMessage(
- message.toString());
+ message.toJson());
while (fragmented.hasNextFragment()) {
doPush(socket, fragmented.getNextFragment());
}
} else {
- doPush(socket, message.toString());
+ doPush(socket, message.toJson());
}
break;
case DISCONNECT_PENDING:
@@ -261,7 +262,7 @@ public class AtmospherePushConnection implements PushConnection {
switch (state) {
case CONNECT_PENDING:
state = State.CONNECTED;
- for (JSONObject message : messageQueue) {
+ for (JsonObject message : messageQueue) {
push(message);
}
messageQueue.clear();
diff --git a/client/src/com/vaadin/client/communication/Date_Serializer.java b/client/src/com/vaadin/client/communication/Date_Serializer.java
index 15ef3869aa..14eb6e4e3d 100644
--- a/client/src/com/vaadin/client/communication/Date_Serializer.java
+++ b/client/src/com/vaadin/client/communication/Date_Serializer.java
@@ -17,11 +17,12 @@ package com.vaadin.client.communication;
import java.util.Date;
-import com.google.gwt.json.client.JSONNumber;
-import com.google.gwt.json.client.JSONValue;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.metadata.Type;
+import elemental.json.Json;
+import elemental.json.JsonValue;
+
/**
* Client side serializer/deserializer for java.util.Date
*
@@ -31,14 +32,14 @@ import com.vaadin.client.metadata.Type;
public class Date_Serializer implements JSONSerializer<Date> {
@Override
- public Date deserialize(Type type, JSONValue jsonValue,
+ public Date deserialize(Type type, JsonValue jsonValue,
ApplicationConnection connection) {
- return new Date((long) ((JSONNumber) jsonValue).doubleValue());
+ return new Date((long) jsonValue.asNumber());
}
@Override
- public JSONValue serialize(Date value, ApplicationConnection connection) {
- return new JSONNumber(value.getTime());
+ public JsonValue serialize(Date value, ApplicationConnection connection) {
+ return Json.create(value.getTime());
}
}
diff --git a/client/src/com/vaadin/client/communication/DiffJSONSerializer.java b/client/src/com/vaadin/client/communication/DiffJSONSerializer.java
index 59575604a1..d433a8964c 100644
--- a/client/src/com/vaadin/client/communication/DiffJSONSerializer.java
+++ b/client/src/com/vaadin/client/communication/DiffJSONSerializer.java
@@ -15,9 +15,9 @@
*/
package com.vaadin.client.communication;
-import com.google.gwt.json.client.JSONValue;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.metadata.Type;
+import elemental.json.JsonValue;
public interface DiffJSONSerializer<T> extends JSONSerializer<T> {
/**
@@ -27,6 +27,6 @@ public interface DiffJSONSerializer<T> extends JSONSerializer<T> {
* @param jsonValue
* @param connection
*/
- public void update(T target, Type type, JSONValue jsonValue,
+ public void update(T target, Type type, JsonValue jsonValue,
ApplicationConnection connection);
}
diff --git a/client/src/com/vaadin/client/communication/JSONSerializer.java b/client/src/com/vaadin/client/communication/JSONSerializer.java
index 3327baf842..59e0329ae1 100644
--- a/client/src/com/vaadin/client/communication/JSONSerializer.java
+++ b/client/src/com/vaadin/client/communication/JSONSerializer.java
@@ -16,16 +16,16 @@
package com.vaadin.client.communication;
-import com.google.gwt.json.client.JSONValue;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.metadata.Type;
+import elemental.json.JsonValue;
/**
* 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.
* <p>
* The {@link #serialize(Object, ApplicationConnection)} and
- * {@link #deserialize(Type, JSONValue, ApplicationConnection)} methods must be
+ * {@link #deserialize(Type, JsonValue, ApplicationConnection)} methods must be
* symmetric so they can be chained and produce the original result (or an equal
* result).
* <p>
@@ -53,12 +53,12 @@ public interface JSONSerializer<T> {
*
* @return A deserialized object
*/
- T deserialize(Type type, JSONValue jsonValue,
+ T deserialize(Type type, JsonValue jsonValue,
ApplicationConnection connection);
/**
* Serialize the given object into JSON. Must be compatible with
- * {@link #deserialize(Type, JSONValue, ApplicationConnection)} and also
+ * {@link #deserialize(Type, JsonValue, ApplicationConnection)} and also
* with the server side JsonCodec.decodeCustomType method.
*
* @param value
@@ -67,6 +67,6 @@ public interface JSONSerializer<T> {
* the application connection providing the context
* @return A JSON serialized version of the object
*/
- JSONValue serialize(T value, ApplicationConnection connection);
+ JsonValue serialize(T value, ApplicationConnection connection);
}
diff --git a/client/src/com/vaadin/client/communication/JsonDecoder.java b/client/src/com/vaadin/client/communication/JsonDecoder.java
index 37c113bb2f..0ce89c873e 100644
--- a/client/src/com/vaadin/client/communication/JsonDecoder.java
+++ b/client/src/com/vaadin/client/communication/JsonDecoder.java
@@ -24,10 +24,6 @@ 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.client.ApplicationConnection;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.FastStringSet;
@@ -38,15 +34,20 @@ import com.vaadin.client.metadata.Property;
import com.vaadin.client.metadata.Type;
import com.vaadin.shared.Connector;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import elemental.json.JsonType;
+import elemental.json.JsonValue;
+
/**
* 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 {
@@ -72,62 +73,60 @@ 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,
+ public static Object decodeValue(Type type, JsonValue jsonValue,
Object target, ApplicationConnection connection) {
+ String baseTypeName = type.getBaseTypeName();
+ if (baseTypeName.startsWith("elemental.json.Json")) {
+ return jsonValue;
+ }
- // Null is null, regardless of type
- if (jsonValue.isNull() != null) {
+ // Null is null, regardless of type (except JSON)
+ if (jsonValue.getType() == JsonType.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);
+ assert jsonValue.getType() == JsonType.ARRAY;
+ return decodeList(type, (JsonArray) jsonValue, connection);
} else if (Set.class.getName().equals(baseTypeName)) {
- return decodeSet(type, (JSONArray) jsonValue, connection);
+ assert jsonValue.getType() == JsonType.ARRAY;
+ return decodeSet(type, (JsonArray) jsonValue, connection);
} else if (String.class.getName().equals(baseTypeName)) {
- return ((JSONString) jsonValue).stringValue();
+ return jsonValue.asString();
} else if (Integer.class.getName().equals(baseTypeName)) {
- return Integer.valueOf(String.valueOf(jsonValue));
+ return Integer.valueOf((int) jsonValue.asNumber());
} else if (Long.class.getName().equals(baseTypeName)) {
- // TODO handle properly
- return Long.valueOf(String.valueOf(jsonValue));
+ return Long.valueOf((long) jsonValue.asNumber());
} else if (Float.class.getName().equals(baseTypeName)) {
- // TODO handle properly
- return Float.valueOf(String.valueOf(jsonValue));
+ return Float.valueOf((float) jsonValue.asNumber());
} else if (Double.class.getName().equals(baseTypeName)) {
- // TODO handle properly
- return Double.valueOf(String.valueOf(jsonValue));
+ return Double.valueOf(jsonValue.asNumber());
} else if (Boolean.class.getName().equals(baseTypeName)) {
- // TODO handle properly
- return Boolean.valueOf(String.valueOf(jsonValue));
+ return Boolean.valueOf(jsonValue.asString());
} else if (Byte.class.getName().equals(baseTypeName)) {
- // TODO handle properly
- return Byte.valueOf(String.valueOf(jsonValue));
+ return Byte.valueOf((byte) jsonValue.asNumber());
} else if (Character.class.getName().equals(baseTypeName)) {
- // TODO handle properly
- return Character.valueOf(((JSONString) jsonValue).stringValue()
- .charAt(0));
+ return Character.valueOf(jsonValue.asString().charAt(0));
} else if (Connector.class.getName().equals(baseTypeName)) {
return ConnectorMap.get(connection).getConnector(
- ((JSONString) jsonValue).stringValue());
+ jsonValue.asString());
} else {
return decodeObject(type, jsonValue, target, connection);
}
}
- private static Object decodeObject(Type type, JSONValue jsonValue,
+ private static Object decodeObject(Type type, JsonValue jsonValue,
Object target, ApplicationConnection connection) {
Profiler.enter("JsonDecoder.decodeObject");
JSONSerializer<Object> serializer = (JSONSerializer<Object>) type
@@ -152,14 +151,12 @@ public class JsonDecoder {
if (target == null) {
target = type.createInstance();
}
- JSONObject jsonObject = jsonValue.isObject();
+ JsonObject jsonObject = (JsonObject) jsonValue;
int size = properties.size();
for (int i = 0; i < size; i++) {
Property property = properties.get(i);
- JSONValue encodedPropertyValue = jsonObject.get(property
- .getName());
- if (encodedPropertyValue == null) {
+ if (!jsonObject.hasKey(property.getName())) {
continue;
}
@@ -173,6 +170,8 @@ public class JsonDecoder {
}
Profiler.leave("JsonDecoder.decodeObject meta data processing");
+ JsonValue encodedPropertyValue = jsonObject.get(property
+ .getName());
Object decodedValue = decodeValue(propertyType,
encodedPropertyValue, propertyReference, connection);
Profiler.enter("JsonDecoder.decodeObject meta data processing");
@@ -194,13 +193,13 @@ public class JsonDecoder {
return !decodedWithoutReference.contains(type.getBaseTypeName());
}
- private static Map<Object, Object> decodeMap(Type type, JSONValue jsonMap,
+ 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) {
+ if (jsonMap.getType() == JsonType.ARRAY) {
+ JsonArray array = (JsonArray) jsonMap;
+ if (array.length() == 0) {
return new HashMap<Object, Object>();
}
}
@@ -209,26 +208,30 @@ public class JsonDecoder {
Type valueType = type.getParameterTypes()[1];
if (keyType.getBaseTypeName().equals(String.class.getName())) {
- return decodeStringMap(valueType, jsonMap, connection);
+ assert jsonMap.getType() == JsonType.OBJECT;
+ return decodeStringMap(valueType, (JsonObject) jsonMap, connection);
} else if (keyType.getBaseTypeName().equals(Connector.class.getName())) {
- return decodeConnectorMap(valueType, jsonMap, connection);
+ assert jsonMap.getType() == JsonType.OBJECT;
+ return decodeConnectorMap(valueType, (JsonObject) jsonMap,
+ connection);
} else {
- return decodeObjectMap(keyType, valueType, jsonMap, connection);
+ assert jsonMap.getType() == JsonType.ARRAY;
+ return decodeObjectMap(keyType, valueType, (JsonArray) jsonMap,
+ connection);
}
}
private static Map<Object, Object> decodeObjectMap(Type keyType,
- Type valueType, JSONValue jsonValue,
+ Type valueType, JsonArray 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);
+ JsonArray keys = jsonValue.get(0);
+ JsonArray values = jsonValue.get(1);
- assert (keys.size() == values.size());
+ assert (keys.length() == values.length());
- for (int i = 0; i < keys.size(); i++) {
+ for (int i = 0; i < keys.length(); i++) {
Object decodedKey = decodeValue(keyType, keys.get(i), null,
connection);
Object decodedValue = decodeValue(valueType, values.get(i), null,
@@ -241,13 +244,12 @@ public class JsonDecoder {
}
private static Map<Object, Object> decodeConnectorMap(Type valueType,
- JSONValue jsonValue, ApplicationConnection connection) {
+ JsonObject jsonMap, ApplicationConnection connection) {
Map<Object, Object> map = new HashMap<Object, Object>();
- JSONObject jsonMap = (JSONObject) jsonValue;
ConnectorMap connectorMap = ConnectorMap.get(connection);
- for (String connectorId : jsonMap.keySet()) {
+ for (String connectorId : jsonMap.keys()) {
Object value = decodeValue(valueType, jsonMap.get(connectorId),
null, connection);
map.put(connectorMap.getConnector(connectorId), value);
@@ -257,12 +259,10 @@ public class JsonDecoder {
}
private static Map<Object, Object> decodeStringMap(Type valueType,
- JSONValue jsonValue, ApplicationConnection connection) {
+ JsonObject jsonMap, ApplicationConnection connection) {
Map<Object, Object> map = new HashMap<Object, Object>();
- JSONObject jsonMap = (JSONObject) jsonValue;
-
- for (String key : jsonMap.keySet()) {
+ for (String key : jsonMap.keys()) {
Object value = decodeValue(valueType, jsonMap.get(key), null,
connection);
map.put(key, value);
@@ -271,7 +271,7 @@ public class JsonDecoder {
return map;
}
- private static List<Object> decodeList(Type type, JSONArray jsonArray,
+ private static List<Object> decodeList(Type type, JsonArray jsonArray,
ApplicationConnection connection) {
List<Object> tokens = new ArrayList<Object>();
decodeIntoCollection(type.getParameterTypes()[0], jsonArray,
@@ -279,7 +279,7 @@ public class JsonDecoder {
return tokens;
}
- private static Set<Object> decodeSet(Type type, JSONArray jsonArray,
+ private static Set<Object> decodeSet(Type type, JsonArray jsonArray,
ApplicationConnection connection) {
Set<Object> tokens = new HashSet<Object>();
decodeIntoCollection(type.getParameterTypes()[0], jsonArray,
@@ -288,12 +288,22 @@ public class JsonDecoder {
}
private static void decodeIntoCollection(Type childType,
- JSONArray jsonArray, ApplicationConnection connection,
+ JsonArray jsonArray, ApplicationConnection connection,
Collection<Object> tokens) {
- for (int i = 0; i < jsonArray.size(); ++i) {
+ for (int i = 0; i < jsonArray.length(); ++i) {
// each entry always has two elements: type and value
- JSONValue entryValue = jsonArray.get(i);
+ JsonValue entryValue = jsonArray.get(i);
tokens.add(decodeValue(childType, entryValue, null, connection));
}
}
+
+ /**
+ * Called by generated deserialization code to treat a generic object as a
+ * JsonValue. This is needed because GWT refuses to directly cast String
+ * typed as Object into a JSO.
+ */
+ public static native <T extends JsonValue> T obj2jso(Object object)
+ /*-{
+ return object;
+ }-*/;
}
diff --git a/client/src/com/vaadin/client/communication/JsonEncoder.java b/client/src/com/vaadin/client/communication/JsonEncoder.java
index 6783e802ec..fad4ad602a 100644
--- a/client/src/com/vaadin/client/communication/JsonEncoder.java
+++ b/client/src/com/vaadin/client/communication/JsonEncoder.java
@@ -22,13 +22,6 @@ 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.client.ApplicationConnection;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.metadata.NoDataException;
@@ -38,6 +31,11 @@ import com.vaadin.shared.Connector;
import com.vaadin.shared.JsonConstants;
import com.vaadin.shared.communication.UidlValue;
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
/**
* Encoder for converting RPC parameters and other values to JSON for transfer
* between the client and the server.
@@ -60,27 +58,27 @@ public class JsonEncoder {
* @param connection
* @return JSON representation of the value
*/
- public static JSONValue encode(Object value, Type type,
+ public static JsonValue encode(Object value, Type type,
ApplicationConnection connection) {
if (null == value) {
- return JSONNull.getInstance();
- } else if (value instanceof JSONValue) {
- return (JSONValue) value;
+ return Json.createNull();
+ } else if (value instanceof JsonValue) {
+ return (JsonValue) value;
} else if (value instanceof String[]) {
String[] array = (String[]) value;
- JSONArray jsonArray = new JSONArray();
+ JsonArray jsonArray = Json.createArray();
for (int i = 0; i < array.length; ++i) {
- jsonArray.set(i, new JSONString(array[i]));
+ jsonArray.set(i, array[i]);
}
return jsonArray;
} else if (value instanceof String) {
- return new JSONString((String) value);
+ return Json.create((String) value);
} else if (value instanceof Boolean) {
- return JSONBoolean.getInstance((Boolean) value);
+ return Json.create((Boolean) value);
} else if (value instanceof Byte) {
- return new JSONNumber((Byte) value);
+ return Json.create((Byte) value);
} else if (value instanceof Character) {
- return new JSONString(String.valueOf(value));
+ return Json.create(String.valueOf(value));
} else if (value instanceof Object[] && type == null) {
// Non-legacy arrays handed by generated serializer
return encodeLegacyObjectArray((Object[]) value, connection);
@@ -90,7 +88,7 @@ public class JsonEncoder {
return encodeMap((Map) value, type, connection);
} else if (value instanceof Connector) {
Connector connector = (Connector) value;
- return new JSONString(connector.getConnectorId());
+ return Json.create(connector.getConnectorId());
} else if (value instanceof Collection) {
return encodeCollection((Collection) value, type, connection);
} else if (value instanceof UidlValue) {
@@ -108,21 +106,21 @@ public class JsonEncoder {
String transportType = getTransportType(value);
if (transportType != null) {
// Send the string value for remaining legacy types
- return new JSONString(String.valueOf(value));
+ return Json.create(String.valueOf(value));
} else if (type != null) {
// And finally try using bean serialization logic
try {
JsArrayObject<Property> properties = type
.getPropertiesAsArray();
- JSONObject jsonObject = new JSONObject();
+ JsonObject jsonObject = Json.createObject();
int size = properties.size();
for (int i = 0; i < size; i++) {
Property property = properties.get(i);
Object propertyValue = property.getValue(value);
Type propertyType = property.getType();
- JSONValue encodedPropertyValue = encode(propertyValue,
+ JsonValue encodedPropertyValue = encode(propertyValue,
propertyType, connection);
jsonObject
.put(property.getName(), encodedPropertyValue);
@@ -141,11 +139,11 @@ public class JsonEncoder {
}
}
- private static JSONValue encodeVariableChange(UidlValue uidlValue,
+ private static JsonValue encodeVariableChange(UidlValue uidlValue,
ApplicationConnection connection) {
Object value = uidlValue.getValue();
- JSONArray jsonArray = new JSONArray();
+ JsonArray jsonArray = Json.createArray();
String transportType = getTransportType(value);
if (transportType == null) {
/*
@@ -159,13 +157,13 @@ public class JsonEncoder {
throw new IllegalArgumentException("Cannot encode object of type "
+ valueType);
}
- jsonArray.set(0, new JSONString(transportType));
+ jsonArray.set(0, Json.create(transportType));
jsonArray.set(1, encode(value, null, connection));
return jsonArray;
}
- private static JSONValue encodeMap(Map<Object, Object> map, Type type,
+ private static JsonValue encodeMap(Map<Object, Object> map, Type type,
ApplicationConnection connection) {
/*
* As we have no info about declared types, we instead select encoding
@@ -174,7 +172,7 @@ public class JsonEncoder {
* server-side decoding must check for. (see #8906)
*/
if (map.isEmpty()) {
- return new JSONArray();
+ return Json.createArray();
}
Object firstKey = map.keySet().iterator().next();
@@ -190,7 +188,7 @@ public class JsonEncoder {
}
}
- private static JSONValue encodeChildValue(Object value,
+ private static JsonValue encodeChildValue(Object value,
Type collectionType, int typeIndex, ApplicationConnection connection) {
if (collectionType == null) {
return encode(new UidlValue(value), null, connection);
@@ -204,35 +202,35 @@ public class JsonEncoder {
}
}
- private static JSONValue encodeObjectMap(Map<Object, Object> map,
+ private static JsonArray encodeObjectMap(Map<Object, Object> map,
Type type, ApplicationConnection connection) {
- JSONArray keys = new JSONArray();
- JSONArray values = new JSONArray();
+ JsonArray keys = Json.createArray();
+ JsonArray values = Json.createArray();
assert type != null : "Should only be used for non-legacy types";
for (Entry<?, ?> entry : map.entrySet()) {
- keys.set(keys.size(),
+ keys.set(keys.length(),
encodeChildValue(entry.getKey(), type, 0, connection));
- values.set(values.size(),
+ values.set(values.length(),
encodeChildValue(entry.getValue(), type, 1, connection));
}
- JSONArray keysAndValues = new JSONArray();
+ JsonArray keysAndValues = Json.createArray();
keysAndValues.set(0, keys);
keysAndValues.set(1, values);
return keysAndValues;
}
- private static JSONValue encodeConnectorMap(Map<Object, Object> map,
+ private static JsonValue encodeConnectorMap(Map<Object, Object> map,
Type type, ApplicationConnection connection) {
- JSONObject jsonMap = new JSONObject();
+ JsonObject jsonMap = Json.createObject();
for (Entry<?, ?> entry : map.entrySet()) {
Connector connector = (Connector) entry.getKey();
- JSONValue encodedValue = encodeChildValue(entry.getValue(), type,
+ JsonValue encodedValue = encodeChildValue(entry.getValue(), type,
1, connection);
jsonMap.put(connector.getConnectorId(), encodedValue);
@@ -241,9 +239,9 @@ public class JsonEncoder {
return jsonMap;
}
- private static JSONValue encodeStringMap(Map<Object, Object> map,
+ private static JsonValue encodeStringMap(Map<Object, Object> map,
Type type, ApplicationConnection connection) {
- JSONObject jsonMap = new JSONObject();
+ JsonObject jsonMap = Json.createObject();
for (Entry<?, ?> entry : map.entrySet()) {
String key = (String) entry.getKey();
@@ -255,14 +253,14 @@ public class JsonEncoder {
return jsonMap;
}
- private static JSONValue encodeEnum(Enum<?> e,
+ private static JsonValue encodeEnum(Enum<?> e,
ApplicationConnection connection) {
- return new JSONString(e.toString());
+ return Json.create(e.toString());
}
- private static JSONValue encodeLegacyObjectArray(Object[] array,
+ private static JsonValue encodeLegacyObjectArray(Object[] array,
ApplicationConnection connection) {
- JSONArray jsonArray = new JSONArray();
+ JsonArray jsonArray = Json.createArray();
for (int i = 0; i < array.length; ++i) {
// TODO handle object graph loops?
Object value = array[i];
@@ -271,12 +269,12 @@ public class JsonEncoder {
return jsonArray;
}
- private static JSONValue encodeCollection(Collection collection, Type type,
+ private static JsonArray encodeCollection(Collection collection, Type type,
ApplicationConnection connection) {
- JSONArray jsonArray = new JSONArray();
+ JsonArray jsonArray = Json.createArray();
int idx = 0;
for (Object o : collection) {
- JSONValue encodedObject = encodeChildValue(o, type, 0, connection);
+ JsonValue encodedObject = encodeChildValue(o, type, 0, connection);
jsonArray.set(idx++, encodedObject);
}
if (collection instanceof Set) {
diff --git a/client/src/com/vaadin/client/communication/PushConnection.java b/client/src/com/vaadin/client/communication/PushConnection.java
index 3bdb18ff1b..8066746dc6 100644
--- a/client/src/com/vaadin/client/communication/PushConnection.java
+++ b/client/src/com/vaadin/client/communication/PushConnection.java
@@ -16,11 +16,11 @@
package com.vaadin.client.communication;
-import com.google.gwt.json.client.JSONObject;
import com.google.gwt.user.client.Command;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler;
import com.vaadin.shared.ui.ui.UIState.PushConfigurationState;
+import elemental.json.JsonObject;
/**
* Represents the client-side endpoint of a bidirectional ("push") communication
@@ -61,7 +61,7 @@ public interface PushConnection {
*
* @see #isActive()
*/
- public void push(JSONObject payload);
+ public void push(JsonObject payload);
/**
* Checks whether this push connection is in a state where it can push
diff --git a/client/src/com/vaadin/client/communication/RpcManager.java b/client/src/com/vaadin/client/communication/RpcManager.java
index 7b706fca2d..f5c3ca9ffb 100644
--- a/client/src/com/vaadin/client/communication/RpcManager.java
+++ b/client/src/com/vaadin/client/communication/RpcManager.java
@@ -18,8 +18,6 @@ package com.vaadin.client.communication;
import java.util.Collection;
-import com.google.gwt.json.client.JSONArray;
-import com.google.gwt.json.client.JSONString;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.ServerConnector;
@@ -30,6 +28,8 @@ import com.vaadin.client.metadata.Type;
import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.MethodInvocation;
+import elemental.json.JsonArray;
+
/**
* Client side RPC manager that can invoke methods based on RPC calls received
* from the server.
@@ -64,7 +64,18 @@ public class RpcManager {
}
}
- private Method getMethod(MethodInvocation invocation) {
+ /**
+ * Gets the method that an invocation targets.
+ *
+ * @param invocation
+ * the method invocation to get the method for
+ *
+ * @since 7.4
+ * @return the method targeted by this invocation
+ */
+ public static Method getMethod(MethodInvocation invocation) {
+ // Implemented here instead of in MethodInovcation since it's in shared
+ // and can't use our Method class.
Type type = new Type(invocation.getInterfaceName(), null);
Method method = type.getMethod(invocation.getMethodName());
return method;
@@ -86,14 +97,14 @@ public class RpcManager {
}
}
- public void parseAndApplyInvocation(JSONArray rpcCall,
+ public MethodInvocation 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);
+ String connectorId = rpcCall.getString(0);
+ String interfaceName = rpcCall.getString(1);
+ String methodName = rpcCall.getString(2);
+ JsonArray parametersJson = rpcCall.getArray(3);
ServerConnector connector = connectorMap.getConnector(connectorId);
@@ -114,14 +125,16 @@ public class RpcManager {
VConsole.log("Server to client RPC call: " + invocation);
applyInvocation(invocation, connector);
}
+
+ return invocation;
}
private void parseMethodParameters(MethodInvocation methodInvocation,
- JSONArray parametersJson, ApplicationConnection connection) {
+ JsonArray parametersJson, ApplicationConnection connection) {
Type[] parameterTypes = getParameterTypes(methodInvocation);
- Object[] parameters = new Object[parametersJson.size()];
- for (int j = 0; j < parametersJson.size(); ++j) {
+ Object[] parameters = new Object[parametersJson.length()];
+ for (int j = 0; j < parametersJson.length(); ++j) {
parameters[j] = JsonDecoder.decodeValue(parameterTypes[j],
parametersJson.get(j), null, connection);
}
diff --git a/client/src/com/vaadin/client/communication/StateChangeEvent.java b/client/src/com/vaadin/client/communication/StateChangeEvent.java
index 6bda41cef2..c2c1ceaea9 100644
--- a/client/src/com/vaadin/client/communication/StateChangeEvent.java
+++ b/client/src/com/vaadin/client/communication/StateChangeEvent.java
@@ -21,16 +21,18 @@ import java.util.Set;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.shared.EventHandler;
-import com.google.gwt.json.client.JSONObject;
import com.vaadin.client.FastStringSet;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.Profiler;
import com.vaadin.client.ServerConnector;
+import com.vaadin.client.Util;
import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
import com.vaadin.client.metadata.NoDataException;
import com.vaadin.client.metadata.Property;
import com.vaadin.client.ui.AbstractConnector;
+import elemental.json.JsonObject;
+
public class StateChangeEvent extends
AbstractServerConnectorEvent<StateChangeHandler> {
/**
@@ -54,7 +56,7 @@ public class StateChangeEvent extends
private boolean initialStateChange = false;
- private JSONObject stateJson;
+ private JsonObject stateJson;
@Override
public Type<StateChangeHandler> getAssociatedType() {
@@ -69,7 +71,7 @@ public class StateChangeEvent extends
* @param changedPropertiesSet
* a set of names of the changed properties
* @deprecated As of 7.0.1, use
- * {@link #StateChangeEvent(ServerConnector, JSONObject, boolean)}
+ * {@link #StateChangeEvent(ServerConnector, JsonObject, boolean)}
* instead for improved performance.
*/
@Deprecated
@@ -93,7 +95,7 @@ public class StateChangeEvent extends
* @param changedProperties
* a set of names of the changed properties
* @deprecated As of 7.0.2, use
- * {@link #StateChangeEvent(ServerConnector, JSONObject, boolean)}
+ * {@link #StateChangeEvent(ServerConnector, JsonObject, boolean)}
* instead for improved performance.
*/
@Deprecated
@@ -114,7 +116,7 @@ public class StateChangeEvent extends
* <code>true</code> if the state change is for a new connector,
* otherwise <code>false</code>
*/
- public StateChangeEvent(ServerConnector connector, JSONObject stateJson,
+ public StateChangeEvent(ServerConnector connector, JsonObject stateJson,
boolean initialStateChange) {
setConnector(connector);
this.stateJson = stateJson;
@@ -203,7 +205,7 @@ public class StateChangeEvent extends
return true;
} else if (stateJson != null) {
// Check whether it's in the json object
- return isInJson(property, stateJson.getJavaScriptObject());
+ return isInJson(property, Util.json2jso(stateJson));
} else {
// Legacy cases
if (changedProperties != null) {
@@ -297,13 +299,13 @@ public class StateChangeEvent extends
* the base name of the current object
*/
@Deprecated
- private static void addJsonFields(JSONObject json,
+ private static void addJsonFields(JsonObject json,
FastStringSet changedProperties, String context) {
- for (String key : json.keySet()) {
+ for (String key : json.keys()) {
String fieldName = context + key;
changedProperties.add(fieldName);
- JSONObject object = json.get(key).isObject();
+ JsonObject object = json.get(key);
if (object != null) {
addJsonFields(object, changedProperties, fieldName + ".");
}
diff --git a/client/src/com/vaadin/client/communication/URLReference_Serializer.java b/client/src/com/vaadin/client/communication/URLReference_Serializer.java
index 4ecdc606d2..71403c3fb3 100644
--- a/client/src/com/vaadin/client/communication/URLReference_Serializer.java
+++ b/client/src/com/vaadin/client/communication/URLReference_Serializer.java
@@ -16,26 +16,28 @@
package com.vaadin.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.client.ApplicationConnection;
import com.vaadin.client.metadata.Type;
import com.vaadin.shared.communication.URLReference;
+import elemental.json.Json;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
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,
+ public URLReference deserialize(Type type, JsonValue jsonValue,
ApplicationConnection connection) {
TranslatedURLReference reference = GWT
.create(TranslatedURLReference.class);
reference.setConnection(connection);
- JSONObject json = (JSONObject) jsonValue;
- if (json.containsKey(URL_FIELD)) {
- JSONValue jsonURL = json.get(URL_FIELD);
+ JsonObject json = (JsonObject) jsonValue;
+ if (json.hasKey(URL_FIELD)) {
+ JsonValue jsonURL = json.get(URL_FIELD);
String URL = (String) JsonDecoder.decodeValue(
new Type(String.class.getName(), null), jsonURL, null,
connection);
@@ -45,9 +47,9 @@ public class URLReference_Serializer implements JSONSerializer<URLReference> {
}
@Override
- public JSONValue serialize(URLReference value,
+ public JsonValue serialize(URLReference value,
ApplicationConnection connection) {
- JSONObject json = new JSONObject();
+ JsonObject json = Json.createObject();
// No type info required for encoding a String...
json.put(URL_FIELD,
JsonEncoder.encode(value.getURL(), null, connection));
diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
index 5df9854038..517d979c8e 100644
--- a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
+++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
@@ -32,6 +32,7 @@ import com.vaadin.client.ConnectorMap;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.VCaption;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.VCssLayout;
import com.vaadin.client.ui.VGridLayout;
@@ -211,10 +212,10 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
// widget to which the path is relative. Otherwise, the current
// implementation simply interprets the path as if baseElement was
// null.
- Widget baseWidget = Util.findWidget(baseElement, null);
+ Widget baseWidget = WidgetUtil.findWidget(baseElement, null);
Widget w = getWidgetFromPath(widgetPath, baseWidget);
- if (w == null || !Util.isAttachedAndDisplayed(w)) {
+ if (w == null || !WidgetUtil.isAttachedAndDisplayed(w)) {
return null;
}
if (parts.length == 1) {
@@ -333,7 +334,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
String childIndexString = part.substring("domChild[".length(),
part.length() - 1);
- if (Util.findWidget(baseElement, null) instanceof VAbstractOrderedLayout) {
+ if (WidgetUtil.findWidget(baseElement, null) instanceof VAbstractOrderedLayout) {
if (element.hasChildNodes()) {
Element e = element.getFirstChildElement().cast();
String cn = e.getClassName();
diff --git a/client/src/com/vaadin/client/connectors/AbstractRendererConnector.java b/client/src/com/vaadin/client/connectors/AbstractRendererConnector.java
new file mode 100644
index 0000000000..f7e3c15ac8
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/AbstractRendererConnector.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.communication.JsonDecoder;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.metadata.NoDataException;
+import com.vaadin.client.metadata.Type;
+import com.vaadin.client.metadata.TypeData;
+import com.vaadin.client.metadata.TypeDataStore;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widgets.Grid.Column;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * An abstract base class for renderer connectors. A renderer connector is used
+ * to link a client-side {@link Renderer} to a server-side
+ * {@link com.vaadin.ui.components.grid.Renderer Renderer}. As a connector, it
+ * can use the regular Vaadin RPC and shared state mechanism to pass additional
+ * state and information between the client and the server. This base class
+ * itself only uses the basic
+ * {@link com.vaadin.shared.communication.SharedState SharedState} and no RPC
+ * interfaces.
+ *
+ * @param <T>
+ * the presentation type of the renderer
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractRendererConnector<T> extends
+ AbstractExtensionConnector {
+
+ private Renderer<T> renderer = null;
+
+ private final Type presentationType = TypeDataStore
+ .getPresentationType(this.getClass());
+
+ protected AbstractRendererConnector() {
+ if (presentationType == null) {
+ throw new IllegalStateException(
+ "No presentation type found for "
+ + getClass().getSimpleName()
+ + ". This may be caused by some unspecified problem in widgetset compilation.");
+ }
+ }
+
+ /**
+ * Returns the renderer associated with this renderer connector.
+ * <p>
+ * A subclass of AbstractRendererConnector should override this method as
+ * shown below. The framework uses
+ * {@link com.google.gwt.core.client.GWT#create(Class) GWT.create(Class)} to
+ * create a renderer based on the return type of the overridden method, but
+ * only if {@link #createRenderer()} is not overridden as well:
+ *
+ * <pre>
+ * public MyRenderer getRenderer() {
+ * return (MyRenderer) super.getRenderer();
+ * }
+ * </pre>
+ *
+ * @return the renderer bound to this connector
+ */
+ public Renderer<T> getRenderer() {
+ if (renderer == null) {
+ renderer = createRenderer();
+ }
+ return renderer;
+ }
+
+ /**
+ * Creates a new Renderer instance associated with this renderer connector.
+ * <p>
+ * You should typically not override this method since the framework by
+ * default generates an implementation that uses
+ * {@link com.google.gwt.core.client.GWT#create(Class)} to create a renderer
+ * of the same type as returned by the most specific override of
+ * {@link #getRenderer()}. If you do override the method, you can't call
+ * <code>super.createRenderer()</code> since the metadata needed for that
+ * implementation is not generated if there's an override of the method.
+ *
+ * @return a new renderer to be used with this connector
+ */
+ protected Renderer<T> createRenderer() {
+ // TODO generate type data
+ Type type = TypeData.getType(getClass());
+ try {
+ Type rendererType = type.getMethod("getRenderer").getReturnType();
+ @SuppressWarnings("unchecked")
+ Renderer<T> instance = (Renderer<T>) rendererType.createInstance();
+ return instance;
+ } catch (NoDataException e) {
+ throw new IllegalStateException(
+ "Default implementation of createRenderer() does not work for "
+ + getClass().getSimpleName()
+ + ". This might be caused by explicitely using "
+ + "super.createRenderer() or some unspecified "
+ + "problem with the widgetset compilation.", e);
+ }
+ }
+
+ /**
+ * Decodes the given JSON value into a value of type T so it can be passed
+ * to the {@link #getRenderer() renderer}.
+ *
+ * @param value
+ * the value to decode
+ * @return the decoded value of {@code value}
+ */
+ public T decode(JsonValue value) {
+ @SuppressWarnings("unchecked")
+ T decodedValue = (T) JsonDecoder.decodeValue(presentationType, value,
+ null, getConnection());
+ return decodedValue;
+ }
+
+ @Override
+ @Deprecated
+ protected void extend(ServerConnector target) {
+ // NOOP
+ }
+
+ /**
+ * Gets the row key for a row object.
+ * <p>
+ * In case this renderer wants be able to identify a row in such a way that
+ * the server also understands it, the row key is used for that. Rows are
+ * identified by unified keys between the client and the server.
+ *
+ * @param row
+ * the row object
+ * @return the row key for the given row
+ */
+ protected String getRowKey(JsonObject row) {
+ final ServerConnector parent = getParent();
+ if (parent instanceof GridConnector) {
+ return ((GridConnector) parent).getRowKey(row);
+ } else {
+ throw new IllegalStateException("Renderers can only be used "
+ + "with a Grid.");
+ }
+ }
+
+ /**
+ * Gets the column id for a column.
+ * <p>
+ * In case this renderer wants be able to identify a column in such a way
+ * that the server also understands it, the column id is used for that.
+ * Columns are identified by unified ids between the client and the server.
+ *
+ * @param column
+ * the column object
+ * @return the column id for the given column
+ */
+ protected String getColumnId(Column<?, JsonObject> column) {
+ final ServerConnector parent = getParent();
+ if (parent instanceof GridConnector) {
+ return ((GridConnector) parent).getColumnId(column);
+ } else {
+ throw new IllegalStateException("Renderers can only be used "
+ + "with a Grid.");
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/client/connectors/ButtonRendererConnector.java b/client/src/com/vaadin/client/connectors/ButtonRendererConnector.java
new file mode 100644
index 0000000000..44c34e3bf4
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/ButtonRendererConnector.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.renderers.ButtonRenderer;
+import com.vaadin.client.renderers.ClickableRenderer.RendererClickHandler;
+import com.vaadin.shared.ui.Connect;
+
+import elemental.json.JsonObject;
+
+/**
+ * A connector for {@link ButtonRenderer}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.ButtonRenderer.class)
+public class ButtonRendererConnector extends ClickableRendererConnector<String> {
+
+ @Override
+ public ButtonRenderer getRenderer() {
+ return (ButtonRenderer) super.getRenderer();
+ }
+
+ @Override
+ protected HandlerRegistration addClickHandler(
+ RendererClickHandler<JsonObject> handler) {
+ return getRenderer().addClickHandler(handler);
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/ClickableRendererConnector.java b/client/src/com/vaadin/client/connectors/ClickableRendererConnector.java
new file mode 100644
index 0000000000..87f88c5106
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/ClickableRendererConnector.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.MouseEventDetailsBuilder;
+import com.vaadin.client.renderers.ClickableRenderer.RendererClickEvent;
+import com.vaadin.client.renderers.ClickableRenderer.RendererClickHandler;
+import com.vaadin.shared.ui.grid.renderers.RendererClickRpc;
+
+import elemental.json.JsonObject;
+
+/**
+ * An abstract base class for {@link ClickableRenderer} connectors.
+ *
+ * @param <T>
+ * the presentation type of the renderer
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract class ClickableRendererConnector<T> extends
+ AbstractRendererConnector<T> {
+
+ HandlerRegistration clickRegistration;
+
+ @Override
+ protected void init() {
+ clickRegistration = addClickHandler(new RendererClickHandler<JsonObject>() {
+ @Override
+ public void onClick(RendererClickEvent<JsonObject> event) {
+ getRpcProxy(RendererClickRpc.class).click(
+ getRowKey(event.getCell().getRow()),
+ getColumnId(event.getCell().getColumn()),
+ MouseEventDetailsBuilder.buildMouseEventDetails(event
+ .getNativeEvent()));
+ }
+ });
+ }
+
+ @Override
+ public void onUnregister() {
+ clickRegistration.removeHandler();
+ }
+
+ protected abstract HandlerRegistration addClickHandler(
+ RendererClickHandler<JsonObject> handler);
+}
diff --git a/client/src/com/vaadin/client/connectors/DateRendererConnector.java b/client/src/com/vaadin/client/connectors/DateRendererConnector.java
new file mode 100644
index 0000000000..30d1db345d
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/DateRendererConnector.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for {@link com.vaadin.ui.components.grid.renderers.DateRenderer
+ * DateRenderer}.
+ * <p>
+ * The server-side Renderer operates on dates, but the data is serialized as a
+ * string, and displayed as-is on the client side. This is to be able to support
+ * the server's locale.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.DateRenderer.class)
+public class DateRendererConnector extends TextRendererConnector {
+ // No implementation needed
+}
diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java
new file mode 100644
index 0000000000..827e366de0
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/GridConnector.java
@@ -0,0 +1,964 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+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.user.client.ui.Widget;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.client.MouseEventDetailsBuilder;
+import com.vaadin.client.annotations.OnStateChange;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.connectors.RpcDataSourceConnector.RpcDataSource;
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.ui.AbstractFieldConnector;
+import com.vaadin.client.ui.AbstractHasComponentsConnector;
+import com.vaadin.client.ui.SimpleManagedLayout;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.CellStyleGenerator;
+import com.vaadin.client.widget.grid.EditorHandler;
+import com.vaadin.client.widget.grid.RowReference;
+import com.vaadin.client.widget.grid.RowStyleGenerator;
+import com.vaadin.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.GridClickEvent;
+import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
+import com.vaadin.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.client.widget.grid.events.SelectAllHandler;
+import com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel;
+import com.vaadin.client.widget.grid.selection.SelectionEvent;
+import com.vaadin.client.widget.grid.selection.SelectionHandler;
+import com.vaadin.client.widget.grid.selection.SelectionModelMulti;
+import com.vaadin.client.widget.grid.selection.SelectionModelNone;
+import com.vaadin.client.widget.grid.selection.SelectionModelSingle;
+import com.vaadin.client.widget.grid.sort.SortEvent;
+import com.vaadin.client.widget.grid.sort.SortHandler;
+import com.vaadin.client.widget.grid.sort.SortOrder;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.client.widgets.Grid.FooterCell;
+import com.vaadin.client.widgets.Grid.FooterRow;
+import com.vaadin.client.widgets.Grid.HeaderCell;
+import com.vaadin.client.widgets.Grid.HeaderRow;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.EditorClientRpc;
+import com.vaadin.shared.ui.grid.EditorServerRpc;
+import com.vaadin.shared.ui.grid.GridClientRpc;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * Connects the client side {@link Grid} widget with the server side
+ * {@link com.vaadin.ui.components.grid.Grid} component.
+ * <p>
+ * The Grid is typed to JSONObject. The structure of the JSONObject is described
+ * at {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List)
+ * DataProviderRpc.setRowData(int, List)}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.Grid.class)
+public class GridConnector extends AbstractHasComponentsConnector implements
+ SimpleManagedLayout {
+
+ private static final class CustomCellStyleGenerator implements
+ CellStyleGenerator<JsonObject> {
+ @Override
+ public String getStyle(CellReference<JsonObject> cellReference) {
+ JsonObject row = cellReference.getRow();
+ if (!row.hasKey(GridState.JSONKEY_CELLSTYLES)) {
+ return null;
+ }
+
+ Column<?, JsonObject> column = cellReference.getColumn();
+ if (!(column instanceof CustomGridColumn)) {
+ // Selection checkbox column
+ return null;
+ }
+ CustomGridColumn c = (CustomGridColumn) column;
+
+ JsonObject cellStylesObject = row
+ .getObject(GridState.JSONKEY_CELLSTYLES);
+ assert cellStylesObject != null;
+
+ if (cellStylesObject.hasKey(c.id)) {
+ return cellStylesObject.getString(c.id);
+ } else {
+ return null;
+ }
+ }
+
+ }
+
+ private static final class CustomRowStyleGenerator implements
+ RowStyleGenerator<JsonObject> {
+ @Override
+ public String getStyle(RowReference<JsonObject> rowReference) {
+ JsonObject row = rowReference.getRow();
+ if (row.hasKey(GridState.JSONKEY_ROWSTYLE)) {
+ return row.getString(GridState.JSONKEY_ROWSTYLE);
+ } else {
+ return null;
+ }
+ }
+
+ }
+
+ /**
+ * Custom implementation of the custom grid column using a JSONObject to
+ * represent the cell value and String as a column type.
+ */
+ private class CustomGridColumn extends Grid.Column<Object, JsonObject> {
+
+ private final String id;
+
+ private AbstractRendererConnector<Object> rendererConnector;
+
+ private AbstractFieldConnector editorConnector;
+
+ public CustomGridColumn(String id,
+ AbstractRendererConnector<Object> rendererConnector) {
+ super(rendererConnector.getRenderer());
+ this.rendererConnector = rendererConnector;
+ this.id = id;
+ }
+
+ @Override
+ public Object getValue(final JsonObject obj) {
+ final JsonObject rowData = obj.getObject(GridState.JSONKEY_DATA);
+
+ if (rowData.hasKey(id)) {
+ final JsonValue columnValue = rowData.get(id);
+
+ return rendererConnector.decode(columnValue);
+ }
+
+ return null;
+ }
+
+ /*
+ * Only used to check that the renderer connector will not change during
+ * the column lifetime.
+ *
+ * TODO remove once support for changing renderers is implemented
+ */
+ private AbstractRendererConnector<Object> getRendererConnector() {
+ return rendererConnector;
+ }
+
+ private AbstractFieldConnector getEditorConnector() {
+ return editorConnector;
+ }
+
+ private void setEditorConnector(AbstractFieldConnector editorConnector) {
+ this.editorConnector = editorConnector;
+ }
+ }
+
+ /*
+ * An editor handler using Vaadin RPC to manage the editor state.
+ */
+ private class CustomEditorHandler implements EditorHandler<JsonObject> {
+
+ private EditorServerRpc rpc = getRpcProxy(EditorServerRpc.class);
+
+ private EditorRequest<?> currentRequest = null;
+ private boolean serverInitiated = false;
+
+ public CustomEditorHandler() {
+ registerRpc(EditorClientRpc.class, new EditorClientRpc() {
+
+ @Override
+ public void bind(final int rowIndex) {
+ // call this finally to avoid issues with editing on init
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ GridConnector.this.getWidget().editRow(rowIndex);
+ }
+ });
+ }
+
+ @Override
+ public void cancel(int rowIndex) {
+ serverInitiated = true;
+ GridConnector.this.getWidget().cancelEditor();
+ }
+
+ @Override
+ public void confirmBind(final boolean bindSucceeded) {
+ endRequest(bindSucceeded);
+ }
+
+ @Override
+ public void confirmSave(boolean saveSucceeded) {
+ endRequest(saveSucceeded);
+ }
+ });
+ }
+
+ @Override
+ public void bind(EditorRequest<JsonObject> request) {
+ startRequest(request);
+ rpc.bind(request.getRowIndex());
+ }
+
+ @Override
+ public void save(EditorRequest<JsonObject> request) {
+ startRequest(request);
+ rpc.save(request.getRowIndex());
+ }
+
+ @Override
+ public void cancel(EditorRequest<JsonObject> request) {
+ if (!handleServerInitiated(request)) {
+ // No startRequest as we don't get (or need)
+ // a confirmation from the server
+ rpc.cancel(request.getRowIndex());
+ }
+ }
+
+ @Override
+ public Widget getWidget(Grid.Column<?, JsonObject> column) {
+ assert column != null;
+
+ if (column instanceof CustomGridColumn) {
+ AbstractFieldConnector c = ((CustomGridColumn) column)
+ .getEditorConnector();
+ return c != null ? c.getWidget() : null;
+ } else {
+ throw new IllegalStateException("Unexpected column type: "
+ + column.getClass().getName());
+ }
+ }
+
+ /**
+ * Used to handle the case where the editor calls us because it was
+ * invoked by the server via RPC and not by the client. In that case,
+ * the request can be simply synchronously completed.
+ *
+ * @param request
+ * the request object
+ * @return true if the request was originally triggered by the server,
+ * false otherwise
+ */
+ private boolean handleServerInitiated(EditorRequest<?> request) {
+ assert request != null : "Cannot handle null request";
+ assert currentRequest == null : "Earlier request not yet finished";
+
+ if (serverInitiated) {
+ serverInitiated = false;
+ request.success();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void startRequest(EditorRequest<?> request) {
+ assert currentRequest == null : "Earlier request not yet finished";
+
+ currentRequest = request;
+ }
+
+ private void endRequest(boolean succeeded) {
+ assert currentRequest != null : "Current request was null";
+ /*
+ * Clear current request first to ensure the state is valid if
+ * another request is made in the callback.
+ */
+ EditorRequest<?> request = currentRequest;
+ currentRequest = null;
+ if (succeeded) {
+ request.success();
+ } else {
+ request.fail();
+ }
+ }
+ }
+
+ private class ItemClickHandler implements BodyClickHandler,
+ BodyDoubleClickHandler {
+
+ @Override
+ public void onClick(GridClickEvent event) {
+ if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
+ fireItemClick(event.getTargetCell(), event.getNativeEvent());
+ }
+ }
+
+ @Override
+ public void onDoubleClick(GridDoubleClickEvent event) {
+ if (hasEventListener(GridConstants.ITEM_CLICK_EVENT_ID)) {
+ fireItemClick(event.getTargetCell(), event.getNativeEvent());
+ }
+ }
+
+ private void fireItemClick(CellReference<?> cell, NativeEvent mouseEvent) {
+ String rowKey = getRowKey((JsonObject) cell.getRow());
+ String columnId = getColumnId(cell.getColumn());
+ getRpcProxy(GridServerRpc.class)
+ .itemClick(
+ rowKey,
+ columnId,
+ MouseEventDetailsBuilder
+ .buildMouseEventDetails(mouseEvent));
+ }
+ }
+
+ /**
+ * Maps a generated column id to a grid column instance
+ */
+ private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
+
+ private AbstractRowHandleSelectionModel<JsonObject> selectionModel;
+ private Set<String> selectedKeys = new LinkedHashSet<String>();
+ private List<String> columnOrder = new ArrayList<String>();
+
+ /**
+ * updateFromState is set to true when {@link #updateSelectionFromState()}
+ * makes changes to selection. This flag tells the
+ * {@code internalSelectionChangeHandler} to not send same data straight
+ * back to server. Said listener sets it back to false when handling that
+ * event.
+ */
+ private boolean updatedFromState = false;
+
+ private RpcDataSource dataSource;
+
+ private SelectionHandler<JsonObject> internalSelectionChangeHandler = new SelectionHandler<JsonObject>() {
+ @Override
+ public void onSelect(SelectionEvent<JsonObject> event) {
+ if (event.isBatchedSelection()) {
+ return;
+ }
+ if (!updatedFromState) {
+ for (JsonObject row : event.getRemoved()) {
+ selectedKeys.remove(dataSource.getRowKey(row));
+ }
+
+ for (JsonObject row : event.getAdded()) {
+ selectedKeys.add(dataSource.getRowKey(row));
+ }
+
+ getRpcProxy(GridServerRpc.class).select(
+ new ArrayList<String>(selectedKeys));
+ } else {
+ updatedFromState = false;
+ }
+ }
+ };
+
+ private ItemClickHandler itemClickHandler = new ItemClickHandler();
+
+ private String lastKnownTheme = null;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Grid<JsonObject> getWidget() {
+ return (Grid<JsonObject>) super.getWidget();
+ }
+
+ @Override
+ public GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+
+ // All scroll RPC calls are executed finally to avoid issues on init
+ registerRpc(GridClientRpc.class, new GridClientRpc() {
+ @Override
+ public void scrollToStart() {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ getWidget().scrollToStart();
+ }
+ });
+ }
+
+ @Override
+ public void scrollToEnd() {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ getWidget().scrollToEnd();
+ }
+ });
+ }
+
+ @Override
+ public void scrollToRow(final int row,
+ final ScrollDestination destination) {
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ getWidget().scrollToRow(row, destination);
+ }
+ });
+ }
+ });
+
+ getWidget().addSelectionHandler(internalSelectionChangeHandler);
+
+ /* Item click events */
+ getWidget().addBodyClickHandler(itemClickHandler);
+ getWidget().addBodyDoubleClickHandler(itemClickHandler);
+
+ getWidget().addSortHandler(new SortHandler<JsonObject>() {
+ @Override
+ public void sort(SortEvent<JsonObject> event) {
+ List<SortOrder> order = event.getOrder();
+ String[] columnIds = new String[order.size()];
+ SortDirection[] directions = new SortDirection[order.size()];
+ for (int i = 0; i < order.size(); i++) {
+ SortOrder sortOrder = order.get(i);
+ CustomGridColumn column = (CustomGridColumn) sortOrder
+ .getColumn();
+ columnIds[i] = column.id;
+
+ directions[i] = sortOrder.getDirection();
+ }
+
+ if (!Arrays.equals(columnIds, getState().sortColumns)
+ || !Arrays.equals(directions, getState().sortDirs)) {
+ // Report back to server if changed
+ getRpcProxy(GridServerRpc.class).sort(columnIds,
+ directions, event.isUserOriginated());
+ }
+ }
+ });
+
+ getWidget().addSelectAllHandler(new SelectAllHandler<JsonObject>() {
+
+ @Override
+ public void onSelectAll(SelectAllEvent<JsonObject> event) {
+ getRpcProxy(GridServerRpc.class).selectAll();
+ }
+
+ });
+
+ getWidget().setEditorHandler(new CustomEditorHandler());
+ getLayoutManager().registerDependency(this, getWidget().getElement());
+ layout();
+ }
+
+ @Override
+ public void onStateChanged(final StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ // Column updates
+ if (stateChangeEvent.hasPropertyChanged("columns")) {
+
+ // Remove old columns
+ purgeRemovedColumns();
+
+ // Add new columns
+ for (GridColumnState state : getState().columns) {
+ if (!columnIdToColumn.containsKey(state.id)) {
+ addColumnFromStateChangeEvent(state);
+ }
+ updateColumnFromState(columnIdToColumn.get(state.id), state);
+ }
+ }
+
+ if (stateChangeEvent.hasPropertyChanged("columnOrder")) {
+ if (orderNeedsUpdate(getState().columnOrder)) {
+ updateColumnOrderFromState(getState().columnOrder);
+ }
+ }
+
+ // Header and footer
+ if (stateChangeEvent.hasPropertyChanged("header")) {
+ updateHeaderFromState(getState().header);
+ }
+
+ if (stateChangeEvent.hasPropertyChanged("footer")) {
+ updateFooterFromState(getState().footer);
+ }
+
+ // Selection
+ if (stateChangeEvent.hasPropertyChanged("selectionMode")) {
+ onSelectionModeChange();
+ }
+ if (stateChangeEvent.hasPropertyChanged("selectedKeys")) {
+ updateSelectionFromState();
+ }
+
+ // Sorting
+ if (stateChangeEvent.hasPropertyChanged("sortColumns")
+ || stateChangeEvent.hasPropertyChanged("sortDirs")) {
+ onSortStateChange();
+ }
+
+ // Editor
+ if (stateChangeEvent.hasPropertyChanged("editorEnabled")) {
+ getWidget().setEditorEnabled(getState().editorEnabled);
+ }
+
+ // Frozen columns
+ if (stateChangeEvent.hasPropertyChanged("frozenColumnCount")) {
+ getWidget().setFrozenColumnCount(getState().frozenColumnCount);
+ }
+
+ // Theme features
+ String activeTheme = getConnection().getUIConnector().getActiveTheme();
+ if (lastKnownTheme == null) {
+ lastKnownTheme = activeTheme;
+ } else if (!lastKnownTheme.equals(activeTheme)) {
+ getWidget().resetSizesFromDom();
+ lastKnownTheme = activeTheme;
+ }
+ }
+
+ private void updateColumnOrderFromState(List<String> stateColumnOrder) {
+ CustomGridColumn[] columns = new CustomGridColumn[stateColumnOrder
+ .size()];
+ int i = 0;
+ for (String id : stateColumnOrder) {
+ columns[i] = columnIdToColumn.get(id);
+ i++;
+ }
+ getWidget().setColumnOrder(columns);
+ columnOrder = stateColumnOrder;
+ }
+
+ private boolean orderNeedsUpdate(List<String> stateColumnOrder) {
+ if (stateColumnOrder.size() == columnOrder.size()) {
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!stateColumnOrder.get(i).equals(columnOrder.get(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private void updateHeaderFromState(GridStaticSectionState state) {
+ getWidget().setHeaderVisible(state.visible);
+
+ while (getWidget().getHeaderRowCount() > 0) {
+ getWidget().removeHeaderRow(0);
+ }
+
+ for (RowState rowState : state.rows) {
+ HeaderRow row = getWidget().appendHeaderRow();
+
+ for (CellState cellState : rowState.cells) {
+ CustomGridColumn column = columnIdToColumn
+ .get(cellState.columnId);
+ updateHeaderCellFromState(row.getCell(column), cellState);
+ }
+
+ for (Set<String> group : rowState.cellGroups.keySet()) {
+ Grid.Column<?, ?>[] columns = new Grid.Column<?, ?>[group
+ .size()];
+ CellState cellState = rowState.cellGroups.get(group);
+
+ int i = 0;
+ for (String columnId : group) {
+ columns[i] = columnIdToColumn.get(columnId);
+ i++;
+ }
+
+ // Set state to be the same as first in group.
+ updateHeaderCellFromState(row.join(columns), cellState);
+ }
+
+ if (rowState.defaultRow) {
+ getWidget().setDefaultHeaderRow(row);
+ }
+
+ row.setStyleName(rowState.styleName);
+ }
+ }
+
+ private void updateHeaderCellFromState(HeaderCell cell, CellState cellState) {
+ switch (cellState.type) {
+ case TEXT:
+ cell.setText(cellState.text);
+ break;
+ case HTML:
+ cell.setHtml(cellState.html);
+ break;
+ case WIDGET:
+ ComponentConnector connector = (ComponentConnector) cellState.connector;
+ cell.setWidget(connector.getWidget());
+ break;
+ default:
+ throw new IllegalStateException("unexpected cell type: "
+ + cellState.type);
+ }
+ cell.setStyleName(cellState.styleName);
+ }
+
+ private void updateFooterFromState(GridStaticSectionState state) {
+ getWidget().setFooterVisible(state.visible);
+
+ while (getWidget().getFooterRowCount() > 0) {
+ getWidget().removeFooterRow(0);
+ }
+
+ for (RowState rowState : state.rows) {
+ FooterRow row = getWidget().appendFooterRow();
+
+ for (CellState cellState : rowState.cells) {
+ CustomGridColumn column = columnIdToColumn
+ .get(cellState.columnId);
+ updateFooterCellFromState(row.getCell(column), cellState);
+ }
+
+ for (Set<String> group : rowState.cellGroups.keySet()) {
+ Grid.Column<?, ?>[] columns = new Grid.Column<?, ?>[group
+ .size()];
+ CellState cellState = rowState.cellGroups.get(group);
+
+ int i = 0;
+ for (String columnId : group) {
+ columns[i] = columnIdToColumn.get(columnId);
+ i++;
+ }
+
+ // Set state to be the same as first in group.
+ updateFooterCellFromState(row.join(columns), cellState);
+ }
+
+ row.setStyleName(rowState.styleName);
+ }
+ }
+
+ private void updateFooterCellFromState(FooterCell cell, CellState cellState) {
+ switch (cellState.type) {
+ case TEXT:
+ cell.setText(cellState.text);
+ break;
+ case HTML:
+ cell.setHtml(cellState.html);
+ break;
+ case WIDGET:
+ ComponentConnector connector = (ComponentConnector) cellState.connector;
+ cell.setWidget(connector.getWidget());
+ break;
+ default:
+ throw new IllegalStateException("unexpected cell type: "
+ + cellState.type);
+ }
+ cell.setStyleName(cellState.styleName);
+ }
+
+ /**
+ * Updates a column from a state change event.
+ *
+ * @param columnIndex
+ * The index of the column to update
+ */
+ private void updateColumnFromStateChangeEvent(GridColumnState columnState) {
+ CustomGridColumn column = columnIdToColumn.get(columnState.id);
+
+ updateColumnFromState(column, columnState);
+
+ if (columnState.rendererConnector != column.getRendererConnector()) {
+ throw new UnsupportedOperationException(
+ "Changing column renderer after initialization is currently unsupported");
+ }
+ }
+
+ /**
+ * Adds a new column to the grid widget from a state change event
+ *
+ * @param columnIndex
+ * The index of the column, according to how it
+ */
+ private void addColumnFromStateChangeEvent(GridColumnState state) {
+ @SuppressWarnings("unchecked")
+ CustomGridColumn column = new CustomGridColumn(state.id,
+ ((AbstractRendererConnector<Object>) state.rendererConnector));
+ columnIdToColumn.put(state.id, column);
+
+ /*
+ * Add column to grid. Reordering is handled as a separate problem.
+ */
+ getWidget().addColumn(column);
+ columnOrder.add(state.id);
+ }
+
+ /**
+ * If we have a selection column renderer, we need to offset the index by
+ * one when referring to the column index in the widget.
+ */
+ private int getWidgetColumnIndex(final int columnIndex) {
+ Renderer<Boolean> selectionColumnRenderer = getWidget()
+ .getSelectionModel().getSelectionColumnRenderer();
+ int widgetColumnIndex = columnIndex;
+ if (selectionColumnRenderer != null) {
+ widgetColumnIndex++;
+ }
+ return widgetColumnIndex;
+ }
+
+ /**
+ * Updates the column values from a state
+ *
+ * @param column
+ * The column to update
+ * @param state
+ * The state to get the data from
+ */
+ private static void updateColumnFromState(CustomGridColumn column,
+ GridColumnState state) {
+ column.setWidth(state.width);
+ column.setMinimumWidth(state.minWidth);
+ column.setMaximumWidth(state.maxWidth);
+ column.setExpandRatio(state.expandRatio);
+
+ column.setSortable(state.sortable);
+ column.setEditorConnector((AbstractFieldConnector) state.editorConnector);
+ }
+
+ /**
+ * Removes any orphan columns that has been removed from the state from the
+ * grid
+ */
+ private void purgeRemovedColumns() {
+
+ // Get columns still registered in the state
+ Set<String> columnsInState = new HashSet<String>();
+ for (GridColumnState columnState : getState().columns) {
+ columnsInState.add(columnState.id);
+ }
+
+ // Remove column no longer in state
+ Iterator<String> columnIdIterator = columnIdToColumn.keySet()
+ .iterator();
+ while (columnIdIterator.hasNext()) {
+ String id = columnIdIterator.next();
+ if (!columnsInState.contains(id)) {
+ CustomGridColumn column = columnIdToColumn.get(id);
+ columnIdIterator.remove();
+ getWidget().removeColumn(column);
+ columnOrder.remove(id);
+ }
+ }
+ }
+
+ public void setDataSource(RpcDataSource dataSource) {
+ this.dataSource = dataSource;
+ getWidget().setDataSource(this.dataSource);
+ }
+
+ private void onSelectionModeChange() {
+ SharedSelectionMode mode = getState().selectionMode;
+ if (mode == null) {
+ getLogger().fine("ignored mode change");
+ return;
+ }
+
+ AbstractRowHandleSelectionModel<JsonObject> model = createSelectionModel(mode);
+ if (selectionModel == null
+ || !model.getClass().equals(selectionModel.getClass())) {
+ selectionModel = model;
+ getWidget().setSelectionModel(model);
+ selectedKeys.clear();
+ }
+ }
+
+ @OnStateChange("hasCellStyleGenerator")
+ private void onCellStyleGeneratorChange() {
+ if (getState().hasCellStyleGenerator) {
+ getWidget().setCellStyleGenerator(new CustomCellStyleGenerator());
+ } else {
+ getWidget().setCellStyleGenerator(null);
+ }
+ }
+
+ @OnStateChange("hasRowStyleGenerator")
+ private void onRowStyleGeneratorChange() {
+ if (getState().hasRowStyleGenerator) {
+ getWidget().setRowStyleGenerator(new CustomRowStyleGenerator());
+ } else {
+ getWidget().setRowStyleGenerator(null);
+ }
+ }
+
+ private void updateSelectionFromState() {
+ boolean changed = false;
+
+ List<String> stateKeys = getState().selectedKeys;
+
+ // find new deselections
+ for (String key : selectedKeys) {
+ if (!stateKeys.contains(key)) {
+ changed = true;
+ deselectByHandle(dataSource.getHandleByKey(key));
+ }
+ }
+
+ // find new selections
+ for (String key : stateKeys) {
+ if (!selectedKeys.contains(key)) {
+ changed = true;
+ selectByHandle(dataSource.getHandleByKey(key));
+ }
+ }
+
+ /*
+ * A defensive copy in case the collection in the state is mutated
+ * instead of re-assigned.
+ */
+ selectedKeys = new LinkedHashSet<String>(stateKeys);
+
+ /*
+ * We need to fire this event so that Grid is able to re-render the
+ * selection changes (if applicable).
+ */
+ if (changed) {
+ // At least for now there's no way to send the selected and/or
+ // deselected row data. Some data is only stored as keys
+ updatedFromState = true;
+ getWidget().fireEvent(
+ new SelectionEvent<JsonObject>(getWidget(),
+ (List<JsonObject>) null, null, false));
+ }
+ }
+
+ private void onSortStateChange() {
+ List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ String[] sortColumns = getState().sortColumns;
+ SortDirection[] sortDirs = getState().sortDirs;
+
+ for (int i = 0; i < sortColumns.length; i++) {
+ sortOrder.add(new SortOrder(columnIdToColumn.get(sortColumns[i]),
+ sortDirs[i]));
+ }
+
+ getWidget().setSortOrder(sortOrder);
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ @SuppressWarnings("static-method")
+ private AbstractRowHandleSelectionModel<JsonObject> createSelectionModel(
+ SharedSelectionMode mode) {
+ switch (mode) {
+ case SINGLE:
+ return new SelectionModelSingle<JsonObject>();
+ case MULTI:
+ return new SelectionModelMulti<JsonObject>();
+ case NONE:
+ return new SelectionModelNone<JsonObject>();
+ default:
+ throw new IllegalStateException("unexpected mode value: " + mode);
+ }
+ }
+
+ /**
+ * A workaround method for accessing the protected method
+ * {@code AbstractRowHandleSelectionModel.selectByHandle}
+ */
+ private native void selectByHandle(RowHandle<JsonObject> handle)
+ /*-{
+ var model = this.@com.vaadin.client.connectors.GridConnector::selectionModel;
+ model.@com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel::selectByHandle(*)(handle);
+ }-*/;
+
+ /**
+ * A workaround method for accessing the protected method
+ * {@code AbstractRowHandleSelectionModel.deselectByHandle}
+ */
+ private native void deselectByHandle(RowHandle<JsonObject> handle)
+ /*-{
+ var model = this.@com.vaadin.client.connectors.GridConnector::selectionModel;
+ model.@com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel::deselectByHandle(*)(handle);
+ }-*/;
+
+ /**
+ * Gets the row key for a row object.
+ *
+ * @param row
+ * the row object
+ * @return the key for the given row
+ */
+ public String getRowKey(JsonObject row) {
+ final Object key = dataSource.getRowKey(row);
+ assert key instanceof String : "Internal key was not a String but a "
+ + key.getClass().getSimpleName() + " (" + key + ")";
+ return (String) key;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.client.HasComponentsConnector#updateCaption(com.vaadin.client
+ * .ComponentConnector)
+ */
+ @Override
+ public void updateCaption(ComponentConnector connector) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(
+ ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) {
+ }
+
+ public String getColumnId(Grid.Column<?, ?> column) {
+ if (column instanceof CustomGridColumn) {
+ return ((CustomGridColumn) column).id;
+ }
+ return null;
+ }
+
+ @Override
+ public void layout() {
+ getWidget().onResize();
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/ImageRendererConnector.java b/client/src/com/vaadin/client/connectors/ImageRendererConnector.java
new file mode 100644
index 0000000000..9000ebb1c2
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/ImageRendererConnector.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.communication.JsonDecoder;
+import com.vaadin.client.metadata.TypeDataStore;
+import com.vaadin.client.renderers.ClickableRenderer.RendererClickHandler;
+import com.vaadin.client.renderers.ImageRenderer;
+import com.vaadin.shared.communication.URLReference;
+import com.vaadin.shared.ui.Connect;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * A connector for {@link ImageRenderer}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.ImageRenderer.class)
+public class ImageRendererConnector extends ClickableRendererConnector<String> {
+
+ @Override
+ public ImageRenderer getRenderer() {
+ return (ImageRenderer) super.getRenderer();
+ }
+
+ @Override
+ public String decode(JsonValue value) {
+ return ((URLReference) JsonDecoder.decodeValue(
+ TypeDataStore.getType(URLReference.class), value, null,
+ getConnection())).getURL();
+ }
+
+ @Override
+ protected HandlerRegistration addClickHandler(
+ RendererClickHandler<JsonObject> handler) {
+ return getRenderer().addClickHandler(handler);
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/JavaScriptRendererConnector.java b/client/src/com/vaadin/client/connectors/JavaScriptRendererConnector.java
new file mode 100644
index 0000000000..2670a3e184
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/JavaScriptRendererConnector.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.NativeEvent;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.JavaScriptConnectorHelper;
+import com.vaadin.client.Util;
+import com.vaadin.client.communication.HasJavaScriptConnectorHelper;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.ui.renderer.AbstractJavaScriptRenderer;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * Connector for server-side renderer implemented using JavaScript.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(AbstractJavaScriptRenderer.class)
+public class JavaScriptRendererConnector extends
+ AbstractRendererConnector<JsonValue> implements
+ HasJavaScriptConnectorHelper {
+ private final JavaScriptConnectorHelper helper = new JavaScriptConnectorHelper(
+ this);
+
+ private final JavaScriptObject cellReferenceWrapper = createCellReferenceWrapper(BrowserInfo
+ .get().isIE8());
+
+ @Override
+ protected void init() {
+ super.init();
+ helper.init();
+
+ addGetRowKey(helper.getConnectorWrapper());
+ }
+
+ private static native JavaScriptObject createCellReferenceWrapper(
+ boolean isIE8)
+ /*-{
+ var reference = {};
+ if (isIE8) {
+ // IE8 only supports defineProperty for DOM objects
+ reference = $doc.createElement('div');
+ }
+
+ var setProperty = function(name, getter, setter) {
+ var descriptor = {
+ get: getter
+ }
+ if (setter) {
+ descriptor.set = setter;
+ }
+ Object.defineProperty(reference, name, descriptor);
+ };
+
+ setProperty("element", function() {
+ return reference.target.@CellReference::getElement()();
+ }, null);
+
+ setProperty("rowIndex", function() {
+ return reference.target.@CellReference::getRowIndex()();
+ }, null);
+
+ setProperty("columnIndex", function() {
+ return reference.target.@CellReference::getColumnIndex()();
+ }, null);
+
+ setProperty("colSpan", function() {
+ return reference.target.@RendererCellReference::getColSpan()();
+ }, function(colSpan) {
+ reference.target.@RendererCellReference::setColSpan(*)(colSpan);
+ });
+
+ return reference;
+ }-*/;
+
+ @Override
+ public JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+
+ private native void addGetRowKey(JavaScriptObject wrapper)
+ /*-{
+ var self = this;
+ wrapper.getRowKey = $entry(function(rowIndex) {
+ return @JavaScriptRendererConnector::findRowKey(*)(self, rowIndex);
+ });
+ }-*/;
+
+ private static String findRowKey(JavaScriptRendererConnector connector,
+ int rowIndex) {
+ GridConnector gc = (GridConnector) connector.getParent();
+ JsonObject row = gc.getWidget().getDataSource().getRow(rowIndex);
+ return connector.getRowKey(row);
+ }
+
+ private boolean hasFunction(String name) {
+ return hasFunction(helper.getConnectorWrapper(), name);
+ }
+
+ private static native boolean hasFunction(JavaScriptObject wrapper,
+ String name)
+ /*-{
+ return typeof wrapper[name] === 'function';
+ }-*/;
+
+ @Override
+ protected Renderer<JsonValue> createRenderer() {
+ helper.ensureJavascriptInited();
+
+ if (!hasFunction("render")) {
+ throw new RuntimeException("JavaScriptRenderer "
+ + helper.getInitFunctionName()
+ + " must have a function named 'render'");
+ }
+
+ final boolean hasInit = hasFunction("init");
+ final boolean hasDestroy = hasFunction("destroy");
+ final boolean hasOnActivate = hasFunction("onActivate");
+ final boolean hasGetConsumedEvents = hasFunction("getConsumedEvents");
+ final boolean hasOnBrowserEvent = hasFunction("onBrowserEvent");
+
+ return new ComplexRenderer<JsonValue>() {
+ @Override
+ public void render(RendererCellReference cell, JsonValue data) {
+ render(helper.getConnectorWrapper(), getJsCell(cell),
+ Util.json2jso(data));
+ }
+
+ private JavaScriptObject getJsCell(CellReference<?> cell) {
+ updateCellReference(cellReferenceWrapper, cell);
+ return cellReferenceWrapper;
+ }
+
+ public native void render(JavaScriptObject wrapper,
+ JavaScriptObject cell, JavaScriptObject data)
+ /*-{
+ wrapper.render(cell, data);
+ }-*/;
+
+ @Override
+ public void init(RendererCellReference cell) {
+ if (hasInit) {
+ init(helper.getConnectorWrapper(), getJsCell(cell));
+ }
+ }
+
+ private native void init(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ wrapper.init(cell);
+ }-*/;
+
+ private native void updateCellReference(
+ JavaScriptObject cellWrapper, CellReference<?> target)
+ /*-{
+ cellWrapper.target = target;
+ }-*/;
+
+ @Override
+ public void destroy(RendererCellReference cell) {
+ if (hasDestroy) {
+ destory(helper.getConnectorWrapper(), getJsCell(cell));
+ } else {
+ super.destroy(cell);
+ }
+ }
+
+ private native void destory(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ wrapper.destory(cell);
+ }-*/;
+
+ @Override
+ public boolean onActivate(CellReference<?> cell) {
+ if (hasOnActivate) {
+ return onActivate(helper.getConnectorWrapper(),
+ getJsCell(cell));
+ } else {
+ return super.onActivate(cell);
+ }
+ }
+
+ private native boolean onActivate(JavaScriptObject wrapper,
+ JavaScriptObject cell)
+ /*-{
+ return !!wrapper.onActivate(cell);
+ }-*/;
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ if (hasGetConsumedEvents) {
+ JsArrayString events = getConsumedEvents(helper
+ .getConnectorWrapper());
+
+ ArrayList<String> list = new ArrayList<String>(
+ events.length());
+ for (int i = 0; i < events.length(); i++) {
+ list.add(events.get(i));
+ }
+ return list;
+ } else {
+ return super.getConsumedEvents();
+ }
+ }
+
+ private native JsArrayString getConsumedEvents(
+ JavaScriptObject wrapper)
+ /*-{
+ var rawEvents = wrapper.getConsumedEvents();
+ var events = [];
+ for(var i = 0; i < rawEvents.length; i++) {
+ events[i] = ""+rawEvents[i];
+ }
+ return events;
+ }-*/;
+
+ @Override
+ public boolean onBrowserEvent(CellReference<?> cell,
+ NativeEvent event) {
+ if (hasOnBrowserEvent) {
+ return onBrowserEvent(helper.getConnectorWrapper(),
+ getJsCell(cell), event);
+ } else {
+ return super.onBrowserEvent(cell, event);
+ }
+ }
+
+ private native boolean onBrowserEvent(JavaScriptObject wrapper,
+ JavaScriptObject cell, NativeEvent event)
+ /*-{
+ return !!wrapper.onBrowserEvent(cell, event);
+ }-*/;
+ };
+ }
+
+ @Override
+ public JsonValue decode(JsonValue value) {
+ // Let the js logic decode the raw json that the server sent
+ return value;
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+ helper.onUnregister();
+ }
+
+ @Override
+ public JavaScriptConnectorHelper getJavascriptConnectorHelper() {
+ return helper;
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/NumberRendererConnector.java b/client/src/com/vaadin/client/connectors/NumberRendererConnector.java
new file mode 100644
index 0000000000..84b319710d
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/NumberRendererConnector.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for
+ * {@link com.vaadin.ui.components.grid.renderers.NumberRenderer NumberRenderer}
+ * .
+ * <p>
+ * The server-side Renderer operates on numbers, but the data is serialized as a
+ * string, and displayed as-is on the client side. This is to be able to support
+ * the server's locale.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.NumberRenderer.class)
+public class NumberRendererConnector extends TextRendererConnector {
+ // no implementation needed
+}
diff --git a/client/src/com/vaadin/client/connectors/ObjectRendererConnector.java b/client/src/com/vaadin/client/connectors/ObjectRendererConnector.java
new file mode 100644
index 0000000000..877eaaa569
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/ObjectRendererConnector.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.client.renderers.TextRenderer;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for {@link com.vaadin.ui.renderer.ObjectRenderer the server side
+ * ObjectRenderer}.
+ * <p>
+ * This uses a {@link TextRenderer} to actually render the contents, as the
+ * object is already converted into a string server-side.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.ObjectRenderer.class)
+public class ObjectRendererConnector extends AbstractRendererConnector<String> {
+
+ @Override
+ public TextRenderer getRenderer() {
+ return (TextRenderer) super.getRenderer();
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/ProgressBarRendererConnector.java b/client/src/com/vaadin/client/connectors/ProgressBarRendererConnector.java
new file mode 100644
index 0000000000..fe410ccbe7
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/ProgressBarRendererConnector.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.client.renderers.ProgressBarRenderer;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for {@link ProgressBarRenderer}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.ProgressBarRenderer.class)
+public class ProgressBarRendererConnector extends
+ AbstractRendererConnector<Double> {
+
+ @Override
+ public ProgressBarRenderer getRenderer() {
+ return (ProgressBarRenderer) super.getRenderer();
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
new file mode 100644
index 0000000000..f8d6ebcb62
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import java.util.ArrayList;
+
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.data.AbstractRemoteDataSource;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+
+/**
+ * Connects a Vaadin server-side container data source to a Grid. This is
+ * currently implemented as an Extension hardcoded to support a specific
+ * connector type. This will be changed once framework support for something
+ * more flexible has been implemented.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.data.RpcDataProviderExtension.class)
+public class RpcDataSourceConnector extends AbstractExtensionConnector {
+
+ public class RpcDataSource extends AbstractRemoteDataSource<JsonObject> {
+
+ protected RpcDataSource() {
+ registerRpc(DataProviderRpc.class, new DataProviderRpc() {
+ @Override
+ public void setRowData(int firstRow, JsonArray rowArray) {
+ ArrayList<JsonObject> rows = new ArrayList<JsonObject>(
+ rowArray.length());
+ for (int i = 0; i < rowArray.length(); i++) {
+ JsonObject rowObject = rowArray.getObject(i);
+ rows.add(rowObject);
+ }
+
+ dataSource.setRowData(firstRow, rows);
+ }
+
+ @Override
+ public void removeRowData(int firstRow, int count) {
+ dataSource.removeRowData(firstRow, count);
+ }
+
+ @Override
+ public void insertRowData(int firstRow, int count) {
+ dataSource.insertRowData(firstRow, count);
+ }
+
+ @Override
+ public void resetDataAndSize(int size) {
+ dataSource.resetDataAndSize(size);
+ }
+ });
+ }
+
+ private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class);
+
+ @Override
+ protected void requestRows(int firstRowIndex, int numberOfRows,
+ RequestRowsCallback<JsonObject> callback) {
+ /*
+ * If you're looking at this code because you want to learn how to
+ * use AbstactRemoteDataSource, please look somewhere else instead.
+ *
+ * We're not doing things in the conventional way with the callback
+ * here since Vaadin doesn't directly support RPC with return
+ * values. We're instead asking the server to push us some data, and
+ * when we receive pushed data, we just push it along to the
+ * underlying cache in the same way no matter if it was a genuine
+ * push or just a result of us requesting rows.
+ */
+
+ Range cached = getCachedRange();
+
+ rpcProxy.requestRows(firstRowIndex, numberOfRows,
+ cached.getStart(), cached.length());
+
+ /*
+ * Show the progress indicator if there is a pending data request
+ * and some of the visible rows are being requested. The RPC in
+ * itself will not trigger the indicator since it might just fetch
+ * some rows in the background to fill the cache.
+ *
+ * The indicator will be hidden by the framework when the response
+ * is received (unless another request is already on its way at that
+ * point).
+ */
+ if (getRequestedAvailability().intersects(
+ Range.withLength(firstRowIndex, numberOfRows))) {
+ getConnection().getLoadingIndicator().ensureTriggered();
+ }
+ }
+
+ @Override
+ public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+ super.ensureAvailability(firstRowIndex, numberOfRows);
+
+ /*
+ * We trigger the indicator already at this point since the actual
+ * RPC will not be sent right away when waiting for the response to
+ * a previous request.
+ *
+ * Only triggering here would not be enough since the check that
+ * sets isWaitingForData is deferred. We don't want to trigger the
+ * loading indicator here if we don't know that there is actually a
+ * request going on since some other bug might then cause the
+ * loading indicator to not be hidden.
+ */
+ if (isWaitingForData()
+ && !Range.withLength(firstRowIndex, numberOfRows)
+ .isSubsetOf(getCachedRange())) {
+ getConnection().getLoadingIndicator().ensureTriggered();
+ }
+ }
+
+ @Override
+ public String getRowKey(JsonObject row) {
+ if (row.hasKey(GridState.JSONKEY_ROWKEY)) {
+ return row.getString(GridState.JSONKEY_ROWKEY);
+ } else {
+ return null;
+ }
+ }
+
+ public RowHandle<JsonObject> getHandleByKey(Object key) {
+ JsonObject row = Json.createObject();
+ row.put(GridState.JSONKEY_ROWKEY, (String) key);
+ return new RowHandleImpl(row, key);
+ }
+
+ @Override
+ protected void pinHandle(RowHandleImpl handle) {
+ // Server only knows if something is pinned or not. No need to pin
+ // multiple times.
+ boolean pinnedBefore = handle.isPinned();
+ super.pinHandle(handle);
+ if (!pinnedBefore) {
+ rpcProxy.setPinned(getRowKey(handle.getRow()), true);
+ }
+ }
+
+ @Override
+ protected void unpinHandle(RowHandleImpl handle) {
+ // Row data is no longer available after it has been unpinned.
+ String key = getRowKey(handle.getRow());
+ super.unpinHandle(handle);
+ if (!handle.isPinned()) {
+ rpcProxy.setPinned(key, false);
+ }
+
+ }
+ }
+
+ private final RpcDataSource dataSource = new RpcDataSource();
+
+ @Override
+ protected void extend(ServerConnector target) {
+ ((GridConnector) target).setDataSource(dataSource);
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/TextRendererConnector.java b/client/src/com/vaadin/client/connectors/TextRendererConnector.java
new file mode 100644
index 0000000000..3059d3f8bb
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/TextRendererConnector.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.client.renderers.TextRenderer;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for {@link TextRenderer}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.TextRenderer.class)
+public class TextRendererConnector extends AbstractRendererConnector<String> {
+
+ @Override
+ public TextRenderer getRenderer() {
+ return (TextRenderer) super.getRenderer();
+ }
+}
diff --git a/client/src/com/vaadin/client/connectors/UnsafeHtmlRendererConnector.java b/client/src/com/vaadin/client/connectors/UnsafeHtmlRendererConnector.java
new file mode 100644
index 0000000000..420f18427d
--- /dev/null
+++ b/client/src/com/vaadin/client/connectors/UnsafeHtmlRendererConnector.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.connectors;
+
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for {@link UnsafeHtmlRenderer}
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.renderer.HtmlRenderer.class)
+public class UnsafeHtmlRendererConnector extends
+ AbstractRendererConnector<String> {
+
+ public static class UnsafeHtmlRenderer implements Renderer<String> {
+ @Override
+ public void render(RendererCellReference cell, String data) {
+ cell.getElement().setInnerHTML(data);
+ }
+ }
+
+ @Override
+ public UnsafeHtmlRenderer getRenderer() {
+ return (UnsafeHtmlRenderer) super.getRenderer();
+ }
+}
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
new file mode 100644
index 0000000000..0ad1631e19
--- /dev/null
+++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.data;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.vaadin.client.Profiler;
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Base implementation for data sources that fetch data from a remote system.
+ * This class takes care of caching data and communicating with the data source
+ * user. An implementation of this class should override
+ * {@link #requestRows(int, int, RequestRowsCallback)} to trigger asynchronously
+ * loading of data and then pass the loaded data into the provided callback.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type
+ */
+public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
+
+ /**
+ * Callback used by
+ * {@link AbstractRemoteDataSource#requestRows(int, int, RequestRowsCallback)}
+ * to pass data to the underlying implementation when data has been fetched.
+ */
+ public static class RequestRowsCallback<T> {
+ private final Range requestedRange;
+ private final double requestStart;
+ private final AbstractRemoteDataSource<T> source;
+
+ /**
+ * Creates a new callback
+ *
+ * @param source
+ * the data source for which the request is made
+ * @param requestedRange
+ * the requested row range
+ */
+ protected RequestRowsCallback(AbstractRemoteDataSource<T> source,
+ Range requestedRange) {
+ this.source = source;
+ this.requestedRange = requestedRange;
+
+ requestStart = Duration.currentTimeMillis();
+ }
+
+ /**
+ * Called by the
+ * {@link AbstractRemoteDataSource#requestRows(int, int, RequestRowsCallback)}
+ * implementation when data has been received.
+ *
+ * @param rowData
+ * a list of row objects starting at the requested offset
+ * @param totalSize
+ * the total number of rows available at the remote end
+ */
+ public void onResponse(List<T> rowData, int totalSize) {
+ if (source.size != totalSize) {
+ source.resetDataAndSize(totalSize);
+ }
+ source.setRowData(requestedRange.getStart(), rowData);
+ }
+
+ /**
+ * Gets the range of rows that was requested.
+ *
+ * @return the requsted row range
+ */
+ public Range getRequestedRange() {
+ return requestedRange;
+ }
+
+ }
+
+ protected class RowHandleImpl extends RowHandle<T> {
+ private T row;
+ private final Object key;
+
+ public RowHandleImpl(final T row, final Object key) {
+ this.row = row;
+ this.key = key;
+ }
+
+ /**
+ * A method for the data source to update the row data.
+ *
+ * @param row
+ * the updated row object
+ */
+ public void setRow(final T row) {
+ this.row = row;
+ assert getRowKey(row).equals(key) : "The old key does not "
+ + "equal the new key for the given row (old: " + key
+ + ", new :" + getRowKey(row) + ")";
+ }
+
+ @Override
+ public T getRow() throws IllegalStateException {
+ if (isPinned()) {
+ return row;
+ } else {
+ throw new IllegalStateException("The row handle for key " + key
+ + " was not pinned");
+ }
+ }
+
+ public boolean isPinned() {
+ return pinnedRows.containsKey(key);
+ }
+
+ @Override
+ public void pin() {
+ pinHandle(this);
+ }
+
+ @Override
+ public void unpin() throws IllegalStateException {
+ unpinHandle(this);
+ }
+
+ @Override
+ protected boolean equalsExplicit(final Object obj) {
+ if (obj instanceof AbstractRemoteDataSource.RowHandleImpl) {
+ /*
+ * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I
+ * like the @SuppressWarnings more (keeps the line length in
+ * check.)
+ */
+ @SuppressWarnings("unchecked")
+ final RowHandleImpl rhi = (RowHandleImpl) obj;
+ return key.equals(rhi.key);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected int hashCodeExplicit() {
+ return key.hashCode();
+ }
+
+ @Override
+ public void updateRow() {
+ int index = indexOf(row);
+ if (index >= 0) {
+ dataChangeHandler.dataUpdated(index, 1);
+ }
+ }
+ }
+
+ private RequestRowsCallback<T> currentRequestCallback;
+
+ private boolean coverageCheckPending = false;
+
+ private Range requestedAvailability = Range.between(0, 0);
+
+ private Range cached = Range.between(0, 0);
+
+ private final HashMap<Integer, T> indexToRowMap = new HashMap<Integer, T>();
+ private final HashMap<Object, Integer> keyToIndexMap = new HashMap<Object, Integer>();
+
+ private DataChangeHandler dataChangeHandler;
+
+ private CacheStrategy cacheStrategy = new CacheStrategy.DefaultCacheStrategy();
+
+ private final ScheduledCommand coverageChecker = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ coverageCheckPending = false;
+ checkCacheCoverage();
+ }
+ };
+
+ private Map<Object, Integer> pinnedCounts = new HashMap<Object, Integer>();
+ private Map<Object, RowHandleImpl> pinnedRows = new HashMap<Object, RowHandleImpl>();
+ protected Collection<T> temporarilyPinnedRows = Collections.emptySet();
+
+ // Size not yet known
+ private int size = -1;
+
+ private void ensureCoverageCheck() {
+ if (!coverageCheckPending) {
+ coverageCheckPending = true;
+ Scheduler.get().scheduleDeferred(coverageChecker);
+ }
+ }
+
+ /**
+ * Pins a row with given handle. This function can be overridden to do
+ * specific logic related to pinning rows.
+ *
+ * @param handle
+ * row handle to pin
+ */
+ protected void pinHandle(RowHandleImpl handle) {
+ Object key = handle.key;
+ Integer count = pinnedCounts.get(key);
+ if (count == null) {
+ count = Integer.valueOf(0);
+ pinnedRows.put(key, handle);
+ }
+ pinnedCounts.put(key, Integer.valueOf(count.intValue() + 1));
+ }
+
+ /**
+ * Unpins a previously pinned row with given handle. This function can be
+ * overridden to do specific logic related to unpinning rows.
+ *
+ * @param handle
+ * row handle to unpin
+ *
+ * @throws IllegalStateException
+ * if given row handle has not been pinned before
+ */
+ protected void unpinHandle(RowHandleImpl handle)
+ throws IllegalStateException {
+ Object key = handle.key;
+ final Integer count = pinnedCounts.get(key);
+ if (count == null) {
+ throw new IllegalStateException("Row " + handle.getRow()
+ + " with key " + key + " was not pinned to begin with");
+ } else if (count.equals(Integer.valueOf(1))) {
+ pinnedRows.remove(key);
+ pinnedCounts.remove(key);
+ } else {
+ pinnedCounts.put(key, Integer.valueOf(count.intValue() - 1));
+ }
+ }
+
+ @Override
+ public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+ requestedAvailability = Range.withLength(firstRowIndex, numberOfRows);
+
+ /*
+ * Don't request any data right away since the data might be included in
+ * a message that has been received but not yet fully processed.
+ */
+ ensureCoverageCheck();
+ }
+
+ /**
+ * Gets the row index range that was requested by the previous call to
+ * {@link #ensureAvailability(int, int)}.
+ *
+ * @return the requested availability range
+ */
+ public Range getRequestedAvailability() {
+ return requestedAvailability;
+ }
+
+ private void checkCacheCoverage() {
+ if (isWaitingForData()) {
+ // Anyone clearing the waiting status should run this method again
+ return;
+ }
+
+ Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage");
+
+ Range minCacheRange = getMinCacheRange();
+
+ if (!minCacheRange.intersects(cached) || cached.isEmpty()) {
+ /*
+ * Simple case: no overlap between cached data and needed data.
+ * Clear the cache and request new data
+ */
+ indexToRowMap.clear();
+ keyToIndexMap.clear();
+ cached = Range.between(0, 0);
+
+ handleMissingRows(getMaxCacheRange());
+ } else {
+ discardStaleCacheEntries();
+
+ // Might need more rows -> request them
+ if (!minCacheRange.isSubsetOf(cached)) {
+ Range[] missingCachePartition = getMaxCacheRange()
+ .partitionWith(cached);
+ handleMissingRows(missingCachePartition[0]);
+ handleMissingRows(missingCachePartition[2]);
+ } else {
+ dataChangeHandler.dataAvailable(cached.getStart(),
+ cached.length());
+ }
+ }
+
+ Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage");
+ }
+
+ /**
+ * Checks whether this data source is currently waiting for more rows to
+ * become available.
+ *
+ * @return <code>true</code> if waiting for data; otherwise
+ * <code>false</code>
+ */
+ public boolean isWaitingForData() {
+ return currentRequestCallback != null;
+ }
+
+ private void discardStaleCacheEntries() {
+ Range[] cacheParition = cached.partitionWith(getMaxCacheRange());
+ dropFromCache(cacheParition[0]);
+ cached = cacheParition[1];
+ dropFromCache(cacheParition[2]);
+ }
+
+ private void dropFromCache(Range range) {
+ for (int i = range.getStart(); i < range.getEnd(); i++) {
+ T removed = indexToRowMap.remove(Integer.valueOf(i));
+ keyToIndexMap.remove(getRowKey(removed));
+ }
+ }
+
+ private void handleMissingRows(Range range) {
+ if (range.isEmpty()) {
+ return;
+ }
+ currentRequestCallback = new RequestRowsCallback<T>(this, range);
+ requestRows(range.getStart(), range.length(), currentRequestCallback);
+ }
+
+ /**
+ * Triggers fetching rows from the remote data source. The provided callback
+ * should be informed when the requested rows have been received.
+ *
+ * @param firstRowIndex
+ * the index of the first row to fetch
+ * @param numberOfRows
+ * the number of rows to fetch
+ * @param callback
+ * callback to inform when the requested rows are available
+ */
+ protected abstract void requestRows(int firstRowIndex, int numberOfRows,
+ RequestRowsCallback<T> callback);
+
+ @Override
+ public T getRow(int rowIndex) {
+ return indexToRowMap.get(Integer.valueOf(rowIndex));
+ }
+
+ @Override
+ public int indexOf(T row) {
+ Object key = getRowKey(row);
+ if (keyToIndexMap.containsKey(key)) {
+ return keyToIndexMap.get(key);
+ }
+ return -1;
+ }
+
+ @Override
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+ this.dataChangeHandler = dataChangeHandler;
+
+ if (dataChangeHandler != null && !cached.isEmpty()) {
+ // Push currently cached data to the implementation
+ dataChangeHandler.dataUpdated(cached.getStart(), cached.length());
+ dataChangeHandler.dataAvailable(cached.getStart(), cached.length());
+ }
+ }
+
+ /**
+ * Informs this data source that updated data has been sent from the server.
+ *
+ * @param firstRowIndex
+ * the index of the first received row
+ * @param rowData
+ * a list of rows, starting from <code>firstRowIndex</code>
+ */
+ protected void setRowData(int firstRowIndex, List<T> rowData) {
+
+ assert firstRowIndex + rowData.size() <= size();
+
+ Profiler.enter("AbstractRemoteDataSource.setRowData");
+
+ Range received = Range.withLength(firstRowIndex, rowData.size());
+
+ if (isWaitingForData()) {
+ cacheStrategy.onDataArrive(Duration.currentTimeMillis()
+ - currentRequestCallback.requestStart, received.length());
+
+ currentRequestCallback = null;
+ }
+
+ Range maxCacheRange = getMaxCacheRange();
+
+ Range[] partition = received.partitionWith(maxCacheRange);
+
+ Range newUsefulData = partition[1];
+ if (!newUsefulData.isEmpty()) {
+ // Update the parts that are actually inside
+ for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
+ final T row = rowData.get(i - firstRowIndex);
+ indexToRowMap.put(Integer.valueOf(i), row);
+ keyToIndexMap.put(getRowKey(row), Integer.valueOf(i));
+ }
+
+ if (dataChangeHandler != null) {
+ Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
+ dataChangeHandler.dataUpdated(newUsefulData.getStart(),
+ newUsefulData.length());
+ Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
+ }
+
+ // Potentially extend the range
+ if (cached.isEmpty()) {
+ cached = newUsefulData;
+ } else {
+ discardStaleCacheEntries();
+
+ /*
+ * everything might've become stale so we need to re-check for
+ * emptiness.
+ */
+ if (!cached.isEmpty()) {
+ cached = cached.combineWith(newUsefulData);
+ } else {
+ cached = newUsefulData;
+ }
+ }
+ dataChangeHandler.dataAvailable(cached.getStart(), cached.length());
+
+ updatePinnedRows(rowData);
+ }
+
+ if (!partition[0].isEmpty() || !partition[2].isEmpty()) {
+ /*
+ * FIXME
+ *
+ * Got data that we might need in a moment if the container is
+ * updated before the widget settings. Support for this will be
+ * implemented later on.
+ */
+ }
+
+ // Eventually check whether all needed rows are now available
+ ensureCoverageCheck();
+
+ Profiler.leave("AbstractRemoteDataSource.setRowData");
+ }
+
+ private void updatePinnedRows(final List<T> rowData) {
+ for (final T row : rowData) {
+ final Object key = getRowKey(row);
+ final RowHandleImpl handle = pinnedRows.get(key);
+ if (handle != null) {
+ handle.setRow(row);
+ }
+ }
+ }
+
+ /**
+ * Informs this data source that the server has removed data.
+ *
+ * @param firstRowIndex
+ * the index of the first removed row
+ * @param count
+ * the number of removed rows, starting from
+ * <code>firstRowIndex</code>
+ */
+ protected void removeRowData(int firstRowIndex, int count) {
+ Profiler.enter("AbstractRemoteDataSource.removeRowData");
+
+ size -= count;
+
+ // shift indices to fill the cache correctly
+ int firstMoved = Math.max(firstRowIndex + count, cached.getStart());
+ for (int i = firstMoved; i < cached.getEnd(); i++) {
+ moveRowFromIndexToIndex(i, i - count);
+ }
+
+ Range removedRange = Range.withLength(firstRowIndex, count);
+ if (cached.isSubsetOf(removedRange)) {
+ // Whole cache is part of the removal. Empty cache
+ cached = Range.withLength(0, 0);
+ } else if (removedRange.intersects(cached)) {
+ // Removal and cache share some indices. fix accordingly.
+ Range[] partitions = cached.partitionWith(removedRange);
+ Range remainsBefore = partitions[0];
+ Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange
+ .length());
+ cached = remainsBefore.combineWith(transposedRemainsAfter);
+ } else if (removedRange.getEnd() <= cached.getStart()) {
+ // Removal was before the cache. offset the cache.
+ cached = cached.offsetBy(-removedRange.length());
+ }
+
+ assertDataChangeHandlerIsInjected();
+ dataChangeHandler.dataRemoved(firstRowIndex, count);
+ ensureCoverageCheck();
+
+ Profiler.leave("AbstractRemoteDataSource.removeRowData");
+ }
+
+ /**
+ * Informs this data source that new data has been inserted from the server.
+ *
+ * @param firstRowIndex
+ * the destination index of the new row data
+ * @param count
+ * the number of rows inserted
+ */
+ protected void insertRowData(int firstRowIndex, int count) {
+ Profiler.enter("AbstractRemoteDataSource.insertRowData");
+
+ size += count;
+
+ if (firstRowIndex <= cached.getStart()) {
+ Range oldCached = cached;
+ cached = cached.offsetBy(count);
+
+ for (int i = 1; i <= indexToRowMap.size(); i++) {
+ int oldIndex = oldCached.getEnd() - i;
+ int newIndex = cached.getEnd() - i;
+ moveRowFromIndexToIndex(oldIndex, newIndex);
+ }
+ } else if (cached.contains(firstRowIndex)) {
+ int oldCacheEnd = cached.getEnd();
+ /*
+ * We need to invalidate the cache from the inserted row onwards,
+ * since the cache wants to be a contiguous range. It doesn't
+ * support holes.
+ *
+ * If holes were supported, we could shift the higher part of
+ * "cached" and leave a hole the size of "count" in the middle.
+ */
+ cached = cached.splitAt(firstRowIndex)[0];
+
+ for (int i = firstRowIndex; i < oldCacheEnd; i++) {
+ T row = indexToRowMap.remove(Integer.valueOf(i));
+ keyToIndexMap.remove(getRowKey(row));
+ }
+ }
+ assertDataChangeHandlerIsInjected();
+ dataChangeHandler.dataAdded(firstRowIndex, count);
+ ensureCoverageCheck();
+
+ Profiler.leave("AbstractRemoteDataSource.insertRowData");
+ }
+
+ private void moveRowFromIndexToIndex(int oldIndex, int newIndex) {
+ T row = indexToRowMap.remove(oldIndex);
+ if (indexToRowMap.containsKey(newIndex)) {
+ // Old row is about to be overwritten. Remove it from keyCache.
+ T row2 = indexToRowMap.remove(newIndex);
+ if (row2 != null) {
+ keyToIndexMap.remove(getRowKey(row2));
+ }
+ }
+ indexToRowMap.put(newIndex, row);
+ if (row != null) {
+ keyToIndexMap.put(getRowKey(row), newIndex);
+ }
+ }
+
+ /**
+ * Gets the current range of cached rows
+ *
+ * @return the range of currently cached rows
+ */
+ public Range getCachedRange() {
+ return cached;
+ }
+
+ /**
+ * Sets the cache strategy that is used to determine how much data is
+ * fetched and cached.
+ * <p>
+ * The new strategy is immediately used to evaluate whether currently cached
+ * rows should be discarded or new rows should be fetched.
+ *
+ * @param cacheStrategy
+ * a cache strategy implementation, not <code>null</code>
+ */
+ public void setCacheStrategy(CacheStrategy cacheStrategy) {
+ if (cacheStrategy == null) {
+ throw new IllegalArgumentException();
+ }
+
+ if (this.cacheStrategy != cacheStrategy) {
+ this.cacheStrategy = cacheStrategy;
+
+ checkCacheCoverage();
+ }
+ }
+
+ private Range getMinCacheRange() {
+ Range availableDataRange = getAvailableRangeForCache();
+
+ Range minCacheRange = cacheStrategy.getMinCacheRange(
+ requestedAvailability, cached, availableDataRange);
+
+ assert minCacheRange.isSubsetOf(availableDataRange);
+
+ return minCacheRange;
+ }
+
+ private Range getMaxCacheRange() {
+ Range availableDataRange = getAvailableRangeForCache();
+ Range maxCacheRange = cacheStrategy.getMaxCacheRange(
+ requestedAvailability, cached, availableDataRange);
+
+ assert maxCacheRange.isSubsetOf(availableDataRange);
+
+ return maxCacheRange;
+ }
+
+ private Range getAvailableRangeForCache() {
+ int upperBound = size();
+ if (upperBound == -1) {
+ upperBound = requestedAvailability.length();
+ }
+ return Range.withLength(0, upperBound);
+ }
+
+ @Override
+ public RowHandle<T> getHandle(T row) throws IllegalStateException {
+ Object key = getRowKey(row);
+
+ if (key == null) {
+ throw new NullPointerException("key may not be null (row: " + row
+ + ")");
+ }
+
+ if (pinnedRows.containsKey(key)) {
+ return pinnedRows.get(key);
+ } else if (keyToIndexMap.containsKey(key)) {
+ return new RowHandleImpl(row, key);
+ } else {
+ throw new IllegalStateException("The cache of this DataSource "
+ + "does not currently contain the row " + row);
+ }
+ }
+
+ /**
+ * Gets a stable key for the row object.
+ * <p>
+ * This method is a workaround for the fact that there is no means to force
+ * proper implementations for {@link #hashCode()} and
+ * {@link #equals(Object)} methods.
+ * <p>
+ * Since the same row object will be created several times for the same
+ * logical data, the DataSource needs a mechanism to be able to compare two
+ * objects, and figure out whether or not they represent the same data. Even
+ * if all the fields of an entity would be changed, it still could represent
+ * the very same thing (say, a person changes all of her names.)
+ * <p>
+ * A very usual and simple example what this could be, is an unique ID for
+ * this object that would also be stored in a database.
+ *
+ * @param row
+ * the row object for which to get the key
+ * @return a non-null object that uniquely and consistently represents the
+ * row object
+ */
+ abstract public Object getRowKey(T row);
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Updates the size, discarding all cached data. This method is used when
+ * the size of the container is changed without any information about the
+ * structure of the change. In this case, all cached data is discarded to
+ * avoid cache offset issues.
+ * <p>
+ * If you have information about the structure of the change, use
+ * {@link #insertRowData(int, int)} or {@link #removeRowData(int, int)} to
+ * indicate where the inserted or removed rows are located.
+ *
+ * @param newSize
+ * the new size of the container
+ */
+ protected void resetDataAndSize(int newSize) {
+ size = newSize;
+ dropFromCache(getCachedRange());
+ cached = Range.withLength(0, 0);
+ assertDataChangeHandlerIsInjected();
+ dataChangeHandler.resetDataAndSize(newSize);
+ }
+
+ private void assertDataChangeHandlerIsInjected() {
+ assert dataChangeHandler != null : "The dataChangeHandler was "
+ + "called before it was injected. Maybe you tried "
+ + "to manipulate the data in the DataSource's "
+ + "constructor instead of in overriding onAttach() "
+ + "and doing it there?";
+ }
+}
diff --git a/client/src/com/vaadin/client/data/CacheStrategy.java b/client/src/com/vaadin/client/data/CacheStrategy.java
new file mode 100644
index 0000000000..79ce537314
--- /dev/null
+++ b/client/src/com/vaadin/client/data/CacheStrategy.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.data;
+
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Determines what data an {@link AbstractRemoteDataSource} should fetch and
+ * keep cached.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface CacheStrategy {
+ /**
+ * A helper class for creating a simple symmetric cache strategy that uses
+ * the same logic for both rows before and after the currently cached range.
+ * <p>
+ * This simple approach rules out more advanced heuristics that would take
+ * the current scrolling direction or past scrolling behavior into account.
+ */
+ public static abstract class AbstractBasicSymmetricalCacheStrategy
+ implements CacheStrategy {
+
+ @Override
+ public void onDataArrive(double roundTripTime, int rowCount) {
+ // NOP
+ }
+
+ @Override
+ public Range getMinCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange) {
+ int cacheSize = getMinimumCacheSize(displayedRange.length());
+
+ return displayedRange.expand(cacheSize, cacheSize).restrictTo(
+ estimatedAvailableRange);
+ }
+
+ @Override
+ public Range getMaxCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange) {
+ int cacheSize = getMaximumCacheSize(displayedRange.length());
+
+ return displayedRange.expand(cacheSize, cacheSize).restrictTo(
+ estimatedAvailableRange);
+ }
+
+ /**
+ * Gets the maximum number of extra items to cache in one direction.
+ *
+ * @param pageSize
+ * the current number of items used at once
+ * @return maximum of items to cache
+ */
+ public abstract int getMaximumCacheSize(int pageSize);
+
+ /**
+ * Gets the the minimum number of extra items to cache in one direction.
+ *
+ * @param pageSize
+ * the current number of items used at once
+ * @return minimum number of items to cache
+ */
+ public abstract int getMinimumCacheSize(int pageSize);
+ }
+
+ /**
+ * The default cache strategy used by {@link AbstractRemoteDataSource},
+ * using multiples of the page size for determining the minimum and maximum
+ * number of items to keep in the cache. By default, at least three times
+ * the page size both before and after the currently used range are kept in
+ * the cache and items are discarded if there's yet another page size worth
+ * of items cached in either direction.
+ */
+ public static class DefaultCacheStrategy extends
+ AbstractBasicSymmetricalCacheStrategy {
+ private final int minimumRatio;
+ private final int maximumRatio;
+
+ /**
+ * Creates a DefaultCacheStrategy keeping between 3 and 4 pages worth of
+ * data cached both before and after the active range.
+ */
+ public DefaultCacheStrategy() {
+ this(3, 4);
+ }
+
+ /**
+ * Creates a DefaultCacheStrategy with custom ratios for how much data
+ * to cache. The ratios denote how many multiples of the currently used
+ * page size are kept in the cache in each direction.
+ *
+ * @param minimumRatio
+ * the minimum number of pages to keep in the cache in each
+ * direction
+ * @param maximumRatio
+ * the maximum number of pages to keep in the cache in each
+ * direction
+ */
+ public DefaultCacheStrategy(int minimumRatio, int maximumRatio) {
+ this.minimumRatio = minimumRatio;
+ this.maximumRatio = maximumRatio;
+ }
+
+ @Override
+ public int getMinimumCacheSize(int pageSize) {
+ return pageSize * minimumRatio;
+ }
+
+ @Override
+ public int getMaximumCacheSize(int pageSize) {
+ return pageSize * maximumRatio;
+ }
+ }
+
+ /**
+ * Called whenever data requested by the data source has arrived. This
+ * information can e.g. be used for measuring how long it takes to fetch
+ * different number of rows from the server.
+ * <p>
+ * A cache strategy implementation cannot use this information to keep track
+ * of which items are in the cache since the data source might discard items
+ * without notifying the cache strategy.
+ *
+ * @param roundTripTime
+ * the total number of milliseconds elapsed from requesting the
+ * data until the response was passed to the data source
+ * @param rowCount
+ * the number of received rows
+ */
+ public void onDataArrive(double roundTripTime, int rowCount);
+
+ /**
+ * Gets the minimum row range that should be cached. The data source will
+ * fetch new data if the currently cached range does not fill the entire
+ * minimum cache range.
+ *
+ * @param displayedRange
+ * the range of currently displayed rows
+ * @param cachedRange
+ * the range of currently cached rows
+ * @param estimatedAvailableRange
+ * the estimated range of rows available for the data source
+ *
+ * @return the minimum range of rows that should be cached, should at least
+ * include the displayed range and should not exceed the total
+ * estimated available range
+ */
+ public Range getMinCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange);
+
+ /**
+ * Gets the maximum row range that should be cached. The data source will
+ * discard cached rows that are outside the maximum range.
+ *
+ * @param displayedRange
+ * the range of currently displayed rows
+ * @param cachedRange
+ * the range of currently cached rows
+ * @param estimatedAvailableRange
+ * the estimated range of rows available for the data source
+ *
+ * @return the maximum range of rows that should be cached, should at least
+ * include the displayed range and should not exceed the total
+ * estimated available range
+ */
+ public Range getMaxCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange);
+}
diff --git a/client/src/com/vaadin/client/data/DataChangeHandler.java b/client/src/com/vaadin/client/data/DataChangeHandler.java
new file mode 100644
index 0000000000..35f1eafea9
--- /dev/null
+++ b/client/src/com/vaadin/client/data/DataChangeHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.data;
+
+/**
+ * Callback interface used by {@link DataSource} to inform its user about
+ * updates to the data.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface DataChangeHandler {
+ /**
+ * Called when the contents of the data source has changed. If the number of
+ * rows has changed or if rows have been moved around,
+ * {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should
+ * ideally be used instead.
+ *
+ * @param firstRowIndex
+ * the index of the first changed row
+ * @param numberOfRows
+ * the number of changed rows
+ */
+ public void dataUpdated(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Called when rows have been removed from the data source.
+ *
+ * @param firstRowIndex
+ * the index that the first removed row had prior to removal
+ * @param numberOfRows
+ * the number of removed rows
+ */
+ public void dataRemoved(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Called when the new rows have been added to the container.
+ *
+ * @param firstRowIndex
+ * the index of the first added row
+ * @param numberOfRows
+ * the number of added rows
+ */
+ public void dataAdded(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Called when rows requested with
+ * {@link DataSource#ensureAvailability(int, int)} rows are available.
+ *
+ * @param firstRowIndex
+ * the index of the first available row
+ * @param numberOfRows
+ * the number of available rows
+ */
+ public void dataAvailable(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Resets all data and defines a new size for the data.
+ * <p>
+ * This should be used in the cases where the data has changed in some
+ * unverifiable way. I.e. "something happened". This will lead to a
+ * re-rendering of the current Grid viewport
+ *
+ * @param estimatedNewDataSize
+ * the estimated size of the new data set
+ */
+ public void resetDataAndSize(int estimatedNewDataSize);
+}
diff --git a/client/src/com/vaadin/client/data/DataSource.java b/client/src/com/vaadin/client/data/DataSource.java
new file mode 100644
index 0000000000..076226bf5c
--- /dev/null
+++ b/client/src/com/vaadin/client/data/DataSource.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.data;
+
+/**
+ * Source of data for widgets showing lazily loaded data based on indexable
+ * items (e.g. rows) of a specified type. The data source is a lazy view into a
+ * larger data set.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type
+ */
+public interface DataSource<T> {
+
+ /**
+ * A handle that contains information on whether a row should be
+ * {@link #pin() pinned} or {@link #unpin() unpinned}, and also always the
+ * most recent representation for that particular row.
+ *
+ * @param <T>
+ * the row type
+ */
+ public abstract class RowHandle<T> {
+ /**
+ * Gets the most recent representation for the row this handle
+ * represents.
+ *
+ * @return the most recent representation for the row this handle
+ * represents
+ * @throws IllegalStateException
+ * if this row handle isn't currently pinned
+ * @see #pin()
+ */
+ public abstract T getRow() throws IllegalStateException;
+
+ /**
+ * Marks this row as pinned.
+ * <p>
+ * <em>Note:</em> Pinning a row multiple times requires an equal amount
+ * of unpins to free the row from the "pinned" status.
+ * <p>
+ * <em>Technical Note:</em> Pinning a row makes sure that the row object
+ * for a particular set of data is always kept as up to date as the data
+ * source is able to. Since the DataSource might create a new instance
+ * of an object, object references aren't necessarily kept up-to-date.
+ * This is a technical work-around for that.
+ *
+ * @see #unpin()
+ */
+ public abstract void pin();
+
+ /**
+ * Marks this row as unpinned.
+ * <p>
+ * <em>Note:</em> Pinning a row multiple times requires an equal amount
+ * of unpins to free the row from the "pinned" status.
+ * <p>
+ * <em>Technical Note:</em> Pinning a row makes sure that the row object
+ * for a particular set of data is always kept as up to date as the data
+ * source is able to. Since the DataSource might create a new instance
+ * of an object, object references aren't necessarily kept up-to-date.
+ * This is a technical work-around for that.
+ *
+ * @throws IllegalStateException
+ * if this row handle has not been pinned before
+ * @see #pin()
+ */
+ public abstract void unpin() throws IllegalStateException;
+
+ /**
+ * Informs the DataSource that the row data represented by this
+ * RowHandle has been updated. DataChangeHandler for the DataSource
+ * should be informed that parts of data have been updated.
+ *
+ * @see DataChangeHandler#dataUpdated(int, int)
+ */
+ public abstract void updateRow();
+
+ /**
+ * An explicit override for {@link Object#equals(Object)}. This method
+ * should be functionally equivalent to a properly implemented equals
+ * method.
+ * <p>
+ * Having a properly implemented equals method is imperative for
+ * RowHandle to function. Because Java has no mechanism to force an
+ * override of an existing method, we're defining a new method for that
+ * instead.
+ *
+ * @param rowHandle
+ * the reference object with which to compare
+ * @return {@code true} if this object is the same as the obj argument;
+ * {@code false} otherwise.
+ */
+ protected abstract boolean equalsExplicit(Object obj);
+
+ /**
+ * An explicit override for {@link Object#hashCode()}. This method
+ * should be functionally equivalent to a properly implemented hashCode
+ * method.
+ * <p>
+ * Having a properly implemented hashCode method is imperative for
+ * RowHandle to function. Because Java has no mechanism to force an
+ * override of an existing method, we're defining a new method for that
+ * instead.
+ *
+ * @return a hash code value for this object
+ */
+ protected abstract int hashCodeExplicit();
+
+ @Override
+ public int hashCode() {
+ return hashCodeExplicit();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return equalsExplicit(obj);
+ }
+ }
+
+ /**
+ * Informs the data source that data for the given range is needed. A data
+ * source only has one active region at a time, so calling this method
+ * discards the previously set range.
+ * <p>
+ * This method triggers lazy loading of data if necessary. The change
+ * handler registered using {@link #setDataChangeHandler(DataChangeHandler)}
+ * is informed when new data has been loaded.
+ * <p>
+ * After any possible lazy loading and updates are done, the change handler
+ * is informed that new data is available.
+ *
+ * @param firstRowIndex
+ * the index of the first needed row
+ * @param numberOfRows
+ * the number of needed rows
+ */
+ public void ensureAvailability(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Retrieves the data for the row at the given index. If the row data is not
+ * available, returns <code>null</code>.
+ * <p>
+ * This method does not trigger loading of unavailable data.
+ * {@link #ensureAvailability(int, int)} should be used to signal what data
+ * will be needed.
+ *
+ * @param rowIndex
+ * the index of the row to retrieve data for
+ * @return data for the row; or <code>null</code> if no data is available
+ */
+ public T getRow(int rowIndex);
+
+ /**
+ * Returns the number of rows in the data source.
+ *
+ * @return the current size of the data source
+ */
+ public int size();
+
+ /**
+ * Sets a data change handler to inform when data is updated, added or
+ * removed.
+ *
+ * @param dataChangeHandler
+ * the data change handler
+ */
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler);
+
+ /**
+ * Gets a {@link RowHandle} of a row object in the cache.
+ *
+ * @param row
+ * the row object for which to retrieve a row handle
+ * @return a non-<code>null</code> row handle of the given row object
+ * @throw IllegalStateException if this data source cannot be sure whether
+ * or not the given row exists. <em>In practice</em> this usually
+ * means that the row is not currently in this data source's cache.
+ */
+ public RowHandle<T> getHandle(T row);
+
+ /**
+ * Retrieves the index for given row object.
+ * <p>
+ * <em>Note:</em> This method does not verify that the given row object
+ * exists at all in this DataSource.
+ *
+ * @param row
+ * the row object
+ * @return index of the row; or <code>-1</code> if row is not available
+ */
+ int indexOf(T row);
+}
diff --git a/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java b/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java
index 1238d88345..fc9f3856b5 100644
--- a/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java
+++ b/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java
@@ -40,7 +40,6 @@ import com.vaadin.client.ComputedStyle;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.SimpleTree;
-import com.vaadin.client.Util;
import com.vaadin.client.ValueMap;
/**
@@ -112,9 +111,12 @@ public class AnalyzeLayoutsPanel extends FlowPanel {
final ServerConnector parent = connector.getParent();
final String parentId = parent.getConnectorId();
- final Label errorDetails = new Label(Util.getSimpleName(connector)
- + "[" + connector.getConnectorId() + "]" + " inside "
- + Util.getSimpleName(parent));
+ final Label errorDetails = new Label(connector.getClass()
+ .getSimpleName()
+ + "["
+ + connector.getConnectorId()
+ + "]"
+ + " inside " + parent.getClass().getSimpleName());
if (parent instanceof ComponentConnector) {
final ComponentConnector parentConnector = (ComponentConnector) parent;
@@ -171,8 +173,8 @@ public class AnalyzeLayoutsPanel extends FlowPanel {
Highlight.show(connector);
- final SimpleTree errorNode = new SimpleTree(
- Util.getSimpleName(connector) + " id: " + pid);
+ final SimpleTree errorNode = new SimpleTree(connector.getClass()
+ .getSimpleName() + " id: " + pid);
errorNode.addDomHandler(new MouseOverHandler() {
@Override
public void onMouseOver(MouseOverEvent event) {
diff --git a/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java b/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java
index 0b49fa7aaf..0856bb3575 100644
--- a/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java
+++ b/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java
@@ -24,8 +24,8 @@ import com.google.gwt.user.client.ui.HTML;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.ServerConnector;
-import com.vaadin.client.Util;
import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.metadata.NoDataException;
import com.vaadin.client.metadata.Property;
import com.vaadin.client.ui.AbstractConnector;
@@ -51,7 +51,7 @@ public class ConnectorInfoPanel extends FlowPanel {
ignoreProperties.add("id");
String html = getRowHTML("Id", connector.getConnectorId());
- html += getRowHTML("Connector", Util.getSimpleName(connector));
+ html += getRowHTML("Connector", connector.getClass().getSimpleName());
if (connector instanceof ComponentConnector) {
ComponentConnector component = (ComponentConnector) connector;
@@ -61,8 +61,8 @@ public class ConnectorInfoPanel extends FlowPanel {
AbstractComponentState componentState = component.getState();
- html += getRowHTML("Widget",
- Util.getSimpleName(component.getWidget()));
+ html += getRowHTML("Widget", component.getWidget().getClass()
+ .getSimpleName());
html += getRowHTML("Caption", componentState.caption);
html += getRowHTML("Description", componentState.description);
html += getRowHTML("Width", componentState.width + " (actual: "
@@ -95,7 +95,8 @@ public class ConnectorInfoPanel extends FlowPanel {
return "<div class=\"" + VDebugWindow.STYLENAME
+ "-row\"><span class=\"caption\">" + caption
+ "</span><span class=\"value\">"
- + Util.escapeHTML(String.valueOf(value)) + "</span></div>";
+ + WidgetUtil.escapeHTML(String.valueOf(value))
+ + "</span></div>";
}
/**
diff --git a/client/src/com/vaadin/client/debug/internal/HierarchySection.java b/client/src/com/vaadin/client/debug/internal/HierarchySection.java
index 404ac430df..c772a9d267 100644
--- a/client/src/com/vaadin/client/debug/internal/HierarchySection.java
+++ b/client/src/com/vaadin/client/debug/internal/HierarchySection.java
@@ -35,6 +35,7 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.ValueMap;
+import com.vaadin.client.WidgetUtil;
/**
* Provides functionality for examining the UI component hierarchy.
@@ -240,7 +241,7 @@ public class HierarchySection implements Section {
}
if (event.getTypeInt() == Event.ONMOUSEMOVE) {
Highlight.hideAll();
- Element eventTarget = Util.getElementFromPoint(event
+ Element eventTarget = WidgetUtil.getElementFromPoint(event
.getNativeEvent().getClientX(), event.getNativeEvent()
.getClientY());
if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) {
@@ -272,7 +273,7 @@ public class HierarchySection implements Section {
event.consume();
event.getNativeEvent().stopPropagation();
stopFind();
- Element eventTarget = Util.getElementFromPoint(event
+ Element eventTarget = WidgetUtil.getElementFromPoint(event
.getNativeEvent().getClientX(), event.getNativeEvent()
.getClientY());
for (ApplicationConnection a : ApplicationConfiguration
diff --git a/client/src/com/vaadin/client/debug/internal/ProfilerSection.java b/client/src/com/vaadin/client/debug/internal/ProfilerSection.java
index c4fea5cf71..7fb0284f8e 100644
--- a/client/src/com/vaadin/client/debug/internal/ProfilerSection.java
+++ b/client/src/com/vaadin/client/debug/internal/ProfilerSection.java
@@ -16,10 +16,8 @@
package com.vaadin.client.debug.internal;
import java.util.Collection;
-import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@@ -29,6 +27,8 @@ import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.Profiler;
+import com.vaadin.client.Profiler.Node;
+import com.vaadin.client.Profiler.ProfilerResultConsumer;
import com.vaadin.client.SimpleTree;
import com.vaadin.client.ValueMap;
@@ -42,237 +42,6 @@ import com.vaadin.client.ValueMap;
* @see Profiler
*/
public class ProfilerSection implements Section {
- /**
- * Interface for getting data from the {@link Profiler}.
- * <p>
- * <b>Warning!</b> This interface is most likely to change in the future and
- * is therefore defined in this class in an internal package instead of
- * Profiler where it might seem more logical.
- *
- * @since 7.1
- * @author Vaadin Ltd
- */
- public interface ProfilerResultConsumer {
- public void addProfilerData(Node rootNode, List<Node> totals);
-
- public void addBootstrapData(LinkedHashMap<String, Double> timings);
- }
-
- /**
- * A hierarchical representation of the time spent running a named block of
- * code.
- * <p>
- * <b>Warning!</b> This class is most likely to change in the future and is
- * therefore defined in this class in an internal package instead of
- * Profiler where it might seem more logical.
- */
- public static class Node {
- private final String name;
- private final LinkedHashMap<String, Node> children = new LinkedHashMap<String, Node>();
- private double time = 0;
- private int count = 0;
- private double enterTime = 0;
- private double minTime = 1000000000;
- private double maxTime = 0;
-
- /**
- * Create a new node with the given name.
- *
- * @param name
- */
- public Node(String name) {
- this.name = name;
- }
-
- /**
- * Gets the name of the node
- *
- * @return the name of the node
- */
- public String getName() {
- return name;
- }
-
- /**
- * Creates a new child node or retrieves and existing child and updates
- * its total time and hit count.
- *
- * @param name
- * the name of the child
- * @param timestamp
- * the timestamp for when the node is entered
- * @return the child node object
- */
- public Node enterChild(String name, double timestamp) {
- Node child = children.get(name);
- if (child == null) {
- child = new Node(name);
- children.put(name, child);
- }
- child.enterTime = timestamp;
- child.count++;
- return child;
- }
-
- /**
- * Gets the total time spent in this node, including time spent in sub
- * nodes
- *
- * @return the total time spent, in milliseconds
- */
- public double getTimeSpent() {
- return time;
- }
-
- /**
- * Gets the minimum time spent for one invocation of this node,
- * including time spent in sub nodes
- *
- * @return the time spent for the fastest invocation, in milliseconds
- */
- public double getMinTimeSpent() {
- return minTime;
- }
-
- /**
- * Gets the maximum time spent for one invocation of this node,
- * including time spent in sub nodes
- *
- * @return the time spent for the slowest invocation, in milliseconds
- */
- public double getMaxTimeSpent() {
- return maxTime;
- }
-
- /**
- * Gets the number of times this node has been entered
- *
- * @return the number of times the node has been entered
- */
- public int getCount() {
- return count;
- }
-
- /**
- * Gets the total time spent in this node, excluding time spent in sub
- * nodes
- *
- * @return the total time spent, in milliseconds
- */
- public double getOwnTime() {
- double time = getTimeSpent();
- for (Node node : children.values()) {
- time -= node.getTimeSpent();
- }
- return time;
- }
-
- /**
- * Gets the child nodes of this node
- *
- * @return a collection of child nodes
- */
- public Collection<Node> getChildren() {
- return Collections.unmodifiableCollection(children.values());
- }
-
- private void buildRecursiveString(StringBuilder builder, String prefix) {
- if (getName() != null) {
- String msg = getStringRepresentation(prefix);
- builder.append(msg + '\n');
- }
- String childPrefix = prefix + "*";
- for (Node node : children.values()) {
- node.buildRecursiveString(builder, childPrefix);
- }
- }
-
- @Override
- public String toString() {
- return getStringRepresentation("");
- }
-
- public String getStringRepresentation(String prefix) {
- if (getName() == null) {
- return "";
- }
- String msg = prefix + " " + getName() + " in " + getTimeSpent()
- + " ms.";
- if (getCount() > 1) {
- msg += " Invoked "
- + getCount()
- + " times ("
- + roundToSignificantFigures(getTimeSpent() / getCount())
- + " ms per time, min "
- + roundToSignificantFigures(getMinTimeSpent())
- + " ms, max "
- + roundToSignificantFigures(getMaxTimeSpent())
- + " ms).";
- }
- if (!children.isEmpty()) {
- double ownTime = getOwnTime();
- msg += " " + ownTime + " ms spent in own code";
- if (getCount() > 1) {
- msg += " ("
- + roundToSignificantFigures(ownTime / getCount())
- + " ms per time)";
- }
- msg += '.';
- }
- return msg;
- }
-
- private static double roundToSignificantFigures(double num) {
- // Number of significant digits
- int n = 3;
- if (num == 0) {
- return 0;
- }
-
- final double d = Math.ceil(Math.log10(num < 0 ? -num : num));
- final int power = n - (int) d;
-
- final double magnitude = Math.pow(10, power);
- final long shifted = Math.round(num * magnitude);
- return shifted / magnitude;
- }
-
- public void sumUpTotals(Map<String, Node> totals) {
- String name = getName();
- if (name != null) {
- Node totalNode = totals.get(name);
- if (totalNode == null) {
- totalNode = new Node(name);
- totals.put(name, totalNode);
- }
-
- totalNode.time += getOwnTime();
- totalNode.count += getCount();
- totalNode.minTime = Math.min(totalNode.minTime,
- getMinTimeSpent());
- totalNode.maxTime = Math.max(totalNode.maxTime,
- getMaxTimeSpent());
- }
- for (Node node : children.values()) {
- node.sumUpTotals(totals);
- }
- }
-
- /**
- * @param timestamp
- */
- public void leave(double timestamp) {
- double elapsed = (timestamp - enterTime);
- time += elapsed;
- enterTime = 0;
- if (elapsed < minTime) {
- minTime = elapsed;
- }
- if (elapsed > maxTime) {
- maxTime = elapsed;
- }
- }
- }
private static final int MAX_ROWS = 10;
diff --git a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java
index 355565f706..d0b6b10722 100644
--- a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java
+++ b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java
@@ -41,6 +41,7 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.ValueMap;
+import com.vaadin.client.WidgetUtil;
/**
* Provides functionality for picking selectors for Vaadin TestBench.
@@ -62,7 +63,8 @@ public class TestBenchSection implements Section {
String html = "<div class=\"" + VDebugWindow.STYLENAME
+ "-selector\"><span class=\"tb-selector\">"
- + Util.escapeHTML(path.getElementQuery()) + "</span></div>";
+ + WidgetUtil.escapeHTML(path.getElementQuery())
+ + "</span></div>";
setHTML(html);
addMouseOverHandler(this);
@@ -216,7 +218,7 @@ public class TestBenchSection implements Section {
}
if (event.getTypeInt() == Event.ONMOUSEMOVE
|| event.getTypeInt() == Event.ONCLICK) {
- Element eventTarget = Util.getElementFromPoint(event
+ Element eventTarget = WidgetUtil.getElementFromPoint(event
.getNativeEvent().getClientX(), event.getNativeEvent()
.getClientY());
if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) {
@@ -230,8 +232,9 @@ public class TestBenchSection implements Section {
// make sure that not finding the highlight element only
Highlight.hideAll();
- eventTarget = Util.getElementFromPoint(event.getNativeEvent()
- .getClientX(), event.getNativeEvent().getClientY());
+ eventTarget = WidgetUtil.getElementFromPoint(event
+ .getNativeEvent().getClientX(), event.getNativeEvent()
+ .getClientY());
ComponentConnector connector = findConnector(eventTarget);
if (event.getTypeInt() == Event.ONMOUSEMOVE) {
diff --git a/client/src/com/vaadin/client/extensions/ResponsiveConnector.java b/client/src/com/vaadin/client/extensions/ResponsiveConnector.java
index e88025531b..2e1e75f6cd 100644
--- a/client/src/com/vaadin/client/extensions/ResponsiveConnector.java
+++ b/client/src/com/vaadin/client/extensions/ResponsiveConnector.java
@@ -382,7 +382,7 @@ public class ResponsiveConnector extends AbstractExtensionConnector implements
/**
* Forces IE8 to reinterpret CSS rules.
- * {@link com.vaadin.client.Util#forceIE8Redraw(com.google.gwt.dom.client.Element)}
+ * {@link com.vaadin.client.WidgetUtil#forceIE8Redraw(com.google.gwt.dom.client.Element)}
* doesn't work in this case.
*
* @param element
diff --git a/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java b/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java
index f76f5058c5..d48571452e 100644
--- a/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java
+++ b/client/src/com/vaadin/client/extensions/javascriptmanager/JavaScriptManagerConnector.java
@@ -21,8 +21,8 @@ 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.client.ServerConnector;
+import com.vaadin.client.Util;
import com.vaadin.client.communication.JavaScriptMethodInvocation;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.extensions.AbstractExtensionConnector;
@@ -116,7 +116,7 @@ public class JavaScriptManagerConnector extends AbstractExtensionConnector {
}-*/;
public void sendRpc(String name, JsArray<JavaScriptObject> arguments) {
- Object[] parameters = new Object[] { name, new JSONArray(arguments) };
+ Object[] parameters = new Object[] { name, Util.jso2json(arguments) };
/*
* Must invoke manually as the RPC interface can't be used in GWT
diff --git a/client/src/com/vaadin/client/metadata/Method.java b/client/src/com/vaadin/client/metadata/Method.java
index d6b474fabc..8757a9de20 100644
--- a/client/src/com/vaadin/client/metadata/Method.java
+++ b/client/src/com/vaadin/client/metadata/Method.java
@@ -15,6 +15,8 @@
*/
package com.vaadin.client.metadata;
+import com.vaadin.shared.annotations.NoLayout;
+
public class Method {
private final Type type;
@@ -100,4 +102,16 @@ public class Method {
return TypeDataStore.isLastOnly(this);
}
+ /**
+ * Checks whether this method is annotated with {@link NoLayout}.
+ *
+ * @since 7.4
+ *
+ * @return <code>true</code> if this method has a NoLayout annotation;
+ * otherwise <code>false</code>
+ */
+ public boolean isNoLayout() {
+ return TypeDataStore.isNoLayoutRpcMethod(this);
+ }
+
}
diff --git a/client/src/com/vaadin/client/metadata/Property.java b/client/src/com/vaadin/client/metadata/Property.java
index f421a5525b..90b29b32b7 100644
--- a/client/src/com/vaadin/client/metadata/Property.java
+++ b/client/src/com/vaadin/client/metadata/Property.java
@@ -16,6 +16,7 @@
package com.vaadin.client.metadata;
import com.vaadin.shared.annotations.DelegateToWidget;
+import com.vaadin.shared.annotations.NoLayout;
public class Property {
private final Type bean;
@@ -127,4 +128,16 @@ public class Property {
return b.toString();
}
+ /**
+ * Checks whether this property is annotated with {@link NoLayout}.
+ *
+ * @since 7.4
+ *
+ * @return <code>true</code> if this property has a NoLayout annotation;
+ * otherwise <code>false</code>
+ */
+ public boolean isNoLayout() {
+ return TypeDataStore.isNoLayoutProperty(this);
+ }
+
}
diff --git a/client/src/com/vaadin/client/metadata/TypeDataStore.java b/client/src/com/vaadin/client/metadata/TypeDataStore.java
index 7aa952d0f2..46f26f1b25 100644
--- a/client/src/com/vaadin/client/metadata/TypeDataStore.java
+++ b/client/src/com/vaadin/client/metadata/TypeDataStore.java
@@ -25,8 +25,13 @@ import com.vaadin.client.FastStringSet;
import com.vaadin.client.JsArrayObject;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.communication.JSONSerializer;
+import com.vaadin.shared.annotations.NoLayout;
public class TypeDataStore {
+ public static enum MethodAttribute {
+ DELAYED, LAST_ONLY, NO_LAYOUT, NO_LOADING_INDICATOR;
+ }
+
private static final String CONSTRUCTOR_NAME = "!new";
private final FastStringMap<Class<?>> identifiers = FastStringMap.create();
@@ -37,6 +42,8 @@ public class TypeDataStore {
.create();
private final FastStringMap<JsArrayString> delegateToWidgetProperties = FastStringMap
.create();
+ private final FastStringMap<Type> presentationTypes = FastStringMap
+ .create();
/**
* Maps connector class -> state property name -> hander method data
@@ -44,8 +51,8 @@ public class TypeDataStore {
private final FastStringMap<FastStringMap<JsArrayObject<OnStateChangeMethod>>> onStateChangeMethods = FastStringMap
.create();
- private final FastStringSet delayedMethods = FastStringSet.create();
- private final FastStringSet lastOnlyMethods = FastStringSet.create();
+ private final FastStringMap<FastStringSet> methodAttributes = FastStringMap
+ .create();
private final FastStringMap<Type> returnTypes = FastStringMap.create();
private final FastStringMap<Invoker> invokers = FastStringMap.create();
@@ -135,6 +142,10 @@ public class TypeDataStore {
return get().delegateToWidgetProperties.get(type.getBaseTypeName());
}
+ public static Type getPresentationType(Class<?> type) {
+ return get().presentationTypes.get(getType(type).getBaseTypeName());
+ }
+
public void setDelegateToWidget(Class<?> clazz, String propertyName,
String delegateValue) {
Type type = getType(clazz);
@@ -150,6 +161,11 @@ public class TypeDataStore {
typeProperties.push(propertyName);
}
+ public void setPresentationType(Class<?> type, Class<?> presentationType) {
+ presentationTypes.put(getType(type).getBaseTypeName(),
+ getType(presentationType));
+ }
+
public void setReturnType(Class<?> type, String methodName, Type returnType) {
returnTypes.put(new Method(getType(type), methodName).getLookupKey(),
returnType);
@@ -200,20 +216,33 @@ public class TypeDataStore {
}
public static boolean isDelayed(Method method) {
- return get().delayedMethods.contains(method.getLookupKey());
+ return hasMethodAttribute(method, MethodAttribute.DELAYED);
}
- public void setDelayed(Class<?> type, String methodName) {
- delayedMethods.add(getType(type).getMethod(methodName).getLookupKey());
+ public static boolean isNoLoadingIndicator(Method method) {
+ return hasMethodAttribute(method, MethodAttribute.NO_LOADING_INDICATOR);
}
- public static boolean isLastOnly(Method method) {
- return get().lastOnlyMethods.contains(method.getLookupKey());
+ private static boolean hasMethodAttribute(Method method,
+ MethodAttribute attribute) {
+ FastStringSet attributes = get().methodAttributes.get(method
+ .getLookupKey());
+ return attributes != null && attributes.contains(attribute.name());
+ }
+
+ public void setMethodAttribute(Class<?> type, String methodName,
+ MethodAttribute attribute) {
+ String key = getType(type).getMethod(methodName).getLookupKey();
+ FastStringSet attributes = methodAttributes.get(key);
+ if (attributes == null) {
+ attributes = FastStringSet.create();
+ methodAttributes.put(key, attributes);
+ }
+ attributes.add(attribute.name());
}
- public void setLastOnly(Class<?> clazz, String methodName) {
- lastOnlyMethods
- .add(getType(clazz).getMethod(methodName).getLookupKey());
+ public static boolean isLastOnly(Method method) {
+ return hasMethodAttribute(method, MethodAttribute.LAST_ONLY);
}
/**
@@ -334,6 +363,12 @@ public class TypeDataStore {
return typeData[beanName][propertyName].setter !== undefined;
}-*/;
+ private static native boolean hasNoLayout(JavaScriptObject typeData,
+ String beanName, String propertyName)
+ /*-{
+ return typeData[beanName][propertyName].noLayout !== undefined;
+ }-*/;
+
private static native Object getJsPropertyValue(JavaScriptObject typeData,
String beanName, String propertyName, Object beanInstance)
/*-{
@@ -418,4 +453,35 @@ public class TypeDataStore {
propertyHandlers.add(method);
}
}
+
+ /**
+ * Checks whether the provided method is annotated with {@link NoLayout}.
+ *
+ * @param method
+ * the rpc method to check
+ *
+ * @since 7.4
+ *
+ * @return <code>true</code> if the method has a NoLayout annotation;
+ * otherwise <code>false</code>
+ */
+ public static boolean isNoLayoutRpcMethod(Method method) {
+ return hasMethodAttribute(method, MethodAttribute.NO_LAYOUT);
+ }
+
+ /**
+ * Checks whether the provided property is annotated with {@link NoLayout}.
+ *
+ * @param property
+ * the property to check
+ *
+ * @since 7.4
+ *
+ * @return <code>true</code> if the property has a NoLayout annotation;
+ * otherwise <code>false</code>
+ */
+ public static boolean isNoLayoutProperty(Property property) {
+ return hasNoLayout(get().jsTypeData, property.getBeanType()
+ .getSignature(), property.getName());
+ }
}
diff --git a/client/src/com/vaadin/client/renderers/ButtonRenderer.java b/client/src/com/vaadin/client/renderers/ButtonRenderer.java
new file mode 100644
index 0000000000..b173aef60a
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/ButtonRenderer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.user.client.ui.Button;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * A Renderer that displays buttons with textual captions. The values of the
+ * corresponding column are used as the captions. Click handlers can be added to
+ * the renderer, invoked when any of the rendered buttons is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ButtonRenderer extends ClickableRenderer<String, Button> {
+
+ @Override
+ public Button createWidget() {
+ Button b = GWT.create(Button.class);
+ b.addClickHandler(this);
+ b.setStylePrimaryName("v-nativebutton");
+ return b;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, String text, Button button) {
+ button.setText(text);
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/ClickableRenderer.java b/client/src/com/vaadin/client/renderers/ClickableRenderer.java
new file mode 100644
index 0000000000..f5368d31c9
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/ClickableRenderer.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+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.MouseEvent;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.EventCellReference;
+import com.vaadin.client.widgets.Escalator;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * An abstract superclass for renderers that render clickable widgets. Click
+ * handlers can be added to a renderer to listen to click events emitted by all
+ * widgets rendered by the renderer.
+ *
+ * @param <T>
+ * the presentation (column) type
+ * @param <W>
+ * the widget type
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract class ClickableRenderer<T, W extends Widget> extends
+ WidgetRenderer<T, W> implements ClickHandler {
+
+ /**
+ * A handler for {@link RendererClickEvent renderer click events}.
+ *
+ * @param <R>
+ * the row type of the containing Grid
+ *
+ * @see {@link ButtonRenderer#addClickHandler(RendererClickHandler)}
+ */
+ public interface RendererClickHandler<R> extends EventHandler {
+
+ /**
+ * Called when a rendered button is clicked.
+ *
+ * @param event
+ * the event representing the click
+ */
+ void onClick(RendererClickEvent<R> event);
+ }
+
+ /**
+ * An event fired when a widget rendered by a ClickableWidgetRenderer
+ * subclass is clicked.
+ *
+ * @param <R>
+ * the row type of the containing Grid
+ */
+ @SuppressWarnings("rawtypes")
+ public static class RendererClickEvent<R> extends
+ MouseEvent<RendererClickHandler> {
+
+ @SuppressWarnings("unchecked")
+ static final Type<RendererClickHandler> TYPE = new Type<RendererClickHandler>(
+ BrowserEvents.CLICK, new RendererClickEvent());
+
+ private CellReference<R> cell;
+
+ private R row;
+
+ private RendererClickEvent() {
+ }
+
+ /**
+ * Returns the cell of the clicked button.
+ *
+ * @return the cell
+ */
+ public CellReference<R> getCell() {
+ return cell;
+ }
+
+ /**
+ * Returns the data object corresponding to the row of the clicked
+ * button.
+ *
+ * @return the row data object
+ */
+ public R getRow() {
+ return row;
+ }
+
+ @Override
+ public Type<RendererClickHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void dispatch(RendererClickHandler handler) {
+
+ EventTarget target = getNativeEvent().getEventTarget();
+
+ if (!Element.is(target)) {
+ return;
+ }
+
+ Element e = Element.as(target);
+ Grid<R> grid = (Grid<R>) findClosestParentGrid(e);
+
+ cell = findCell(grid, e);
+ row = cell.getRow();
+
+ handler.onClick(this);
+ }
+
+ /**
+ * Returns the cell the given element belongs to.
+ *
+ * @param grid
+ * the grid instance that is queried
+ * @param e
+ * a cell element or the descendant of one
+ * @return the cell or null if the element is not a grid cell or a
+ * descendant of one
+ */
+ private static <T> CellReference<T> findCell(Grid<T> grid, Element e) {
+ RowContainer container = getEscalator(grid).findRowContainer(e);
+ if (container == null) {
+ return null;
+ }
+ Cell cell = container.getCell(e);
+ EventCellReference<T> cellReference = new EventCellReference<T>(
+ grid);
+ cellReference.set(cell);
+ return cellReference;
+ }
+
+ private native static Escalator getEscalator(Grid<?> grid)
+ /*-{
+ return grid.@com.vaadin.client.widgets.Grid::escalator;
+ }-*/;
+
+ /**
+ * Returns the Grid instance containing the given element, if any.
+ * <p>
+ * <strong>Note:</strong> This method may not work reliably if the grid
+ * in question is wrapped in a {@link Composite} <em>unless</em> the
+ * element is inside another widget that is a child of the wrapped grid;
+ * please refer to the note in
+ * {@link WidgetUtil#findWidget(Element, Class) Util.findWidget} for
+ * details.
+ *
+ * @param e
+ * the element whose parent grid to find
+ * @return the parent grid or null if none found.
+ */
+ private static Grid<?> findClosestParentGrid(Element e) {
+ Widget w = WidgetUtil.findWidget(e, null);
+
+ while (w != null && !(w instanceof Grid)) {
+ w = w.getParent();
+ }
+ return (Grid<?>) w;
+ }
+ }
+
+ private HandlerManager handlerManager;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation note:</em> It is the implementing method's
+ * responsibility to add {@code this} as a click handler of the returned
+ * widget, or a widget nested therein, in order to make click events
+ * propagate properly to handlers registered via
+ * {@link #addClickHandler(RendererClickHandler) addClickHandler}.
+ */
+ @Override
+ public abstract W createWidget();
+
+ /**
+ * Adds a click handler to this button renderer. The handler is invoked
+ * every time one of the widgets rendered by this renderer is clicked.
+ * <p>
+ * Note that the row type of the click handler must match the row type of
+ * the containing Grid.
+ *
+ * @param handler
+ * the click handler to be added
+ */
+ public HandlerRegistration addClickHandler(RendererClickHandler<?> handler) {
+ if (handlerManager == null) {
+ handlerManager = new HandlerManager(this);
+ }
+ return handlerManager.addHandler(RendererClickEvent.TYPE, handler);
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ /*
+ * The handler manager is lazily instantiated so it's null iff
+ * addClickHandler is never called.
+ */
+ if (handlerManager != null) {
+ DomEvent.fireNativeEvent(event.getNativeEvent(), handlerManager);
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/ComplexRenderer.java b/client/src/com/vaadin/client/renderers/ComplexRenderer.java
new file mode 100644
index 0000000000..75ea523cdc
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/ComplexRenderer.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import java.util.Collection;
+import java.util.Collections;
+
+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.Style.Visibility;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * Base class for renderers that needs initialization and destruction logic
+ * (override {@link #init(FlyweightCell) and #destroy(FlyweightCell) } and event
+ * handling (see {@link #onBrowserEvent(Cell, NativeEvent)},
+ * {@link #getConsumedEvents()} and {@link #onActivate()}.
+ *
+ * <p>
+ * Also provides a helper method for hiding the cell contents by overriding
+ * {@link #setContentVisible(FlyweightCell, boolean)}
+ *
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public abstract class ComplexRenderer<T> implements Renderer<T> {
+
+ /**
+ * Called at initialization stage. Perform any initialization here e.g.
+ * attach handlers, attach widgets etc.
+ *
+ * @param cell
+ * The cell. Note that the cell is not to be stored outside of
+ * the method as the cell install will change. See
+ * {@link FlyweightCell}
+ */
+ public abstract void init(RendererCellReference cell);
+
+ /**
+ * Called after the cell is deemed to be destroyed and no longer used by the
+ * Grid. Called after the cell element is detached from the DOM.
+ * <p>
+ * The row object in the cell reference will be <code>null</code> since the
+ * row might no longer be present in the data source.
+ *
+ * @param cell
+ * The cell. Note that the cell is not to be stored outside of
+ * the method as the cell install will change. See
+ * {@link FlyweightCell}
+ */
+ public void destroy(RendererCellReference cell) {
+ // Implement if needed
+ }
+
+ /**
+ * Returns the events that the renderer should consume. These are also the
+ * events that the Grid will pass to
+ * {@link #onBrowserEvent(Cell, NativeEvent)} when they occur.
+ *
+ * @return a list of consumed events
+ *
+ * @see com.google.gwt.dom.client.BrowserEvents
+ */
+ public Collection<String> getConsumedEvents() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Called whenever a registered event is triggered in the column the
+ * renderer renders.
+ * <p>
+ * The events that triggers this needs to be returned by the
+ * {@link #getConsumedEvents()} method.
+ * <p>
+ * Returns boolean telling if the event has been completely handled and
+ * should not cause any other actions.
+ *
+ * @param cell
+ * Object containing information about the cell the event was
+ * triggered on.
+ *
+ * @param event
+ * The original DOM event
+ * @return true if event should not be handled by grid
+ */
+ public boolean onBrowserEvent(CellReference<?> cell, NativeEvent event) {
+ return false;
+ }
+
+ /**
+ * Used by Grid to toggle whether to show actual data or just an empty
+ * placeholder while data is loading. This method is invoked whenever a cell
+ * changes between data being available and data missing.
+ * <p>
+ * Default implementation hides content by setting visibility: hidden to all
+ * elements inside the cell. Text nodes are left as is - renderers that add
+ * such to the root element need to implement explicit support hiding them.
+ *
+ * @param cell
+ * The cell
+ * @param hasData
+ * Has the cell content been loaded from the data source
+ *
+ */
+ public void setContentVisible(RendererCellReference cell, boolean hasData) {
+ Element cellElement = cell.getElement();
+ for (int n = 0; n < cellElement.getChildCount(); n++) {
+ Node node = cellElement.getChild(n);
+ if (Element.is(node)) {
+ Element e = Element.as(node);
+ if (hasData) {
+ e.getStyle().clearVisibility();
+ } else {
+ e.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when the cell is activated by pressing <code>enter</code>, double
+ * clicking or performing a double tap on the cell.
+ *
+ * @param cell
+ * the activated cell
+ * @return <code>true</code> if event was handled and should not be
+ * interpreted as a generic gesture by Grid.
+ */
+ public boolean onActivate(CellReference<?> cell) {
+ return false;
+ }
+
+ /**
+ * Called when the renderer is deemed to be destroyed and no longer used by
+ * the Grid.
+ */
+ public void destroy() {
+ // Implement if needed
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/DateRenderer.java b/client/src/com/vaadin/client/renderers/DateRenderer.java
new file mode 100644
index 0000000000..4d15fac724
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/DateRenderer.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.TimeZone;
+import com.google.gwt.i18n.shared.DateTimeFormat;
+import com.google.gwt.i18n.shared.DateTimeFormat.PredefinedFormat;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer for rendering dates into cells
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DateRenderer implements Renderer<Date> {
+
+ private DateTimeFormat format;
+
+ // Calendar is unavailable for GWT
+ @SuppressWarnings("deprecation")
+ private TimeZone timeZone = TimeZone.createTimeZone(new Date()
+ .getTimezoneOffset());
+
+ public DateRenderer() {
+ this(PredefinedFormat.DATE_TIME_SHORT);
+ }
+
+ public DateRenderer(PredefinedFormat format) {
+ this(DateTimeFormat.getFormat(format));
+ }
+
+ public DateRenderer(DateTimeFormat format) {
+ setFormat(format);
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Date date) {
+ String dateStr = format.format(date, timeZone);
+ cell.getElement().setInnerText(dateStr);
+ }
+
+ /**
+ * Gets the format of how the date is formatted.
+ *
+ * @return the format
+ * @see <a
+ * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/shared/DateTimeFormat.html">GWT
+ * documentation on DateTimeFormat</a>
+ */
+ public DateTimeFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the format used for formatting the dates.
+ *
+ * @param format
+ * the format to set
+ * @see <a
+ * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/shared/DateTimeFormat.html">GWT
+ * documentation on DateTimeFormat</a>
+ */
+ public void setFormat(DateTimeFormat format) {
+ if (format == null) {
+ throw new IllegalArgumentException("Format should not be null");
+ }
+ this.format = format;
+ }
+
+ /**
+ * Returns the time zone of the date.
+ *
+ * @return the time zone
+ */
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ /**
+ * Sets the time zone of the the date. By default uses the time zone of the
+ * browser.
+ *
+ * @param timeZone
+ * the timeZone to set
+ */
+ public void setTimeZone(TimeZone timeZone) {
+ if (timeZone == null) {
+ throw new IllegalArgumentException("Timezone should not be null");
+ }
+ this.timeZone = timeZone;
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/HtmlRenderer.java b/client/src/com/vaadin/client/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..ec6dc761f6
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/HtmlRenderer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * Renders a string as HTML into a cell.
+ * <p>
+ * The html string is rendered as is without any escaping. It is up to the
+ * developer to ensure that the html string honors the {@link SafeHtml}
+ * contract. For more information see
+ * {@link SafeHtmlUtils#fromSafeConstant(String)}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see SafeHtmlUtils#fromSafeConstant(String)
+ */
+public class HtmlRenderer implements Renderer<String> {
+
+ @Override
+ public void render(RendererCellReference cell, String htmlString) {
+ cell.getElement().setInnerSafeHtml(
+ SafeHtmlUtils.fromSafeConstant(htmlString));
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/ImageRenderer.java b/client/src/com/vaadin/client/renderers/ImageRenderer.java
new file mode 100644
index 0000000000..b1e8ce5702
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/ImageRenderer.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.Image;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer that renders an image into a cell. Click handlers can be added to
+ * the renderer, invoked every time any of the images rendered by that rendered
+ * is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ImageRenderer extends ClickableRenderer<String, Image> {
+
+ @Override
+ public Image createWidget() {
+ Image image = GWT.create(Image.class);
+ image.addClickHandler(this);
+ return image;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, String url, Image image) {
+ image.setUrl(url);
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/NumberRenderer.java b/client/src/com/vaadin/client/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..a040e8d1ce
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/NumberRenderer.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.i18n.client.NumberFormat;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * Renders a number into a cell using a specific {@link NumberFormat}. By
+ * default uses the default number format returned by
+ * {@link NumberFormat#getDecimalFormat()}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <T>
+ * The number type to render.
+ */
+public class NumberRenderer implements Renderer<Number> {
+
+ private NumberFormat format;
+
+ public NumberRenderer() {
+ this(NumberFormat.getDecimalFormat());
+ }
+
+ public NumberRenderer(NumberFormat format) {
+ setFormat(format);
+ }
+
+ /**
+ * Gets the number format that the number should be formatted in.
+ *
+ * @return the number format used to render the number
+ */
+ public NumberFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the number format to use for formatting the number.
+ *
+ * @param format
+ * the format to use
+ * @throws IllegalArgumentException
+ * when the format is null
+ */
+ public void setFormat(NumberFormat format) throws IllegalArgumentException {
+ if (format == null) {
+ throw new IllegalArgumentException("Format cannot be null");
+ }
+ this.format = format;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Number number) {
+ cell.getElement().setInnerText(format.format(number));
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/ObjectRenderer.java b/client/src/com/vaadin/client/renderers/ObjectRenderer.java
new file mode 100644
index 0000000000..a2c4e7bfc6
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/ObjectRenderer.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer for displaying an object to a string using the
+ * {@link Object#toString()} method.
+ * <p>
+ * If the object is <code>null</code>, then it is rendered as an empty string
+ * instead.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class ObjectRenderer implements Renderer<Object> {
+ @Override
+ public void render(RendererCellReference cell, Object data) {
+ String text = (data != null) ? data.toString() : "";
+ cell.getElement().setInnerText(text);
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/ProgressBarRenderer.java b/client/src/com/vaadin/client/renderers/ProgressBarRenderer.java
new file mode 100644
index 0000000000..5b2c70d274
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/ProgressBarRenderer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.core.shared.GWT;
+import com.vaadin.client.ui.VProgressBar;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * A Renderer that represents a double value as a graphical progress bar.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ProgressBarRenderer extends WidgetRenderer<Double, VProgressBar> {
+
+ @Override
+ public VProgressBar createWidget() {
+ VProgressBar progressBar = GWT.create(VProgressBar.class);
+ progressBar.addStyleDependentName("static");
+ return progressBar;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Double data,
+ VProgressBar progressBar) {
+ if (data == null) {
+ progressBar.setEnabled(false);
+ } else {
+ progressBar.setEnabled(true);
+ progressBar.setState(data.floatValue());
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/Renderer.java b/client/src/com/vaadin/client/renderers/Renderer.java
new file mode 100644
index 0000000000..a3faa1e9df
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/Renderer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Renderer for rending a value &lt;T&gt; into cell.
+ * <p>
+ * You can add a renderer to any column by overring the
+ * {@link GridColumn#getRenderer()} method and returning your own renderer. You
+ * can retrieve the cell element using {@link Cell#getElement()}.
+ *
+ * @param <T>
+ * The column type
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> {
+
+ /**
+ * Called whenever the {@link Grid} updates a cell
+ *
+ * @param cell
+ * The cell. Note that the cell is a flyweight and should not be
+ * stored outside of the method as it will change.
+ *
+ * @param data
+ * The column data object
+ */
+ void render(RendererCellReference cell, T data);
+}
diff --git a/client/src/com/vaadin/client/renderers/TextRenderer.java b/client/src/com/vaadin/client/renderers/TextRenderer.java
new file mode 100644
index 0000000000..3f704fd0b4
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/TextRenderer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * Renderer that renders text into a cell.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class TextRenderer implements Renderer<String> {
+
+ @Override
+ public void render(RendererCellReference cell, String text) {
+ cell.getElement().setInnerText(text);
+ }
+}
diff --git a/client/src/com/vaadin/client/renderers/WidgetRenderer.java b/client/src/com/vaadin/client/renderers/WidgetRenderer.java
new file mode 100644
index 0000000000..668ec7b59e
--- /dev/null
+++ b/client/src/com/vaadin/client/renderers/WidgetRenderer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.renderers;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.widget.grid.RendererCellReference;
+
+/**
+ * A renderer for rendering widgets into cells.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row data type
+ * @param <W>
+ * the Widget type
+ */
+public abstract class WidgetRenderer<T, W extends Widget> extends
+ ComplexRenderer<T> {
+
+ @Override
+ public void init(RendererCellReference cell) {
+ // Implement if needed
+ }
+
+ /**
+ * Creates a widget to attach to a cell. The widgets will be attached to the
+ * cell after the cell element has been attached to DOM.
+ *
+ * @return widget to attach to a cell. All returned instances should be new
+ * widget instances without a parent.
+ */
+ public abstract W createWidget();
+
+ @Override
+ public void render(RendererCellReference cell, T data) {
+ W w = getWidget(cell.getElement());
+ assert w != null : "Widget not found in cell (" + cell.getColumn()
+ + "," + cell.getRow() + ")";
+ render(cell, data, w);
+ }
+
+ /**
+ * Renders a cell with a widget. This provides a way to update any
+ * information in the widget that is cell specific. Do not detach the Widget
+ * here, it will be done automatically by the Grid when the widget is no
+ * longer needed.
+ *
+ * @param cell
+ * the cell to render
+ * @param data
+ * the data of the cell
+ * @param widget
+ * the widget embedded in the cell
+ */
+ public abstract void render(RendererCellReference cell, T data, W widget);
+
+ /**
+ * Returns the widget contained inside the given cell element. Cannot be
+ * called for cells that do not contain a widget.
+ *
+ * @param e
+ * the element inside which to find a widget
+ * @return the widget inside the element
+ */
+ protected W getWidget(TableCellElement e) {
+ W w = getWidget(e, null);
+ assert w != null : "Widget not found inside cell";
+ return w;
+ }
+
+ /**
+ * Returns the widget contained inside the given cell element, or null if it
+ * is not an instance of the given class. Cannot be called for cells that do
+ * not contain a widget.
+ *
+ * @param e
+ * the element inside to find a widget
+ * @param klass
+ * the type of the widget to find
+ * @return the widget inside the element, or null if its type does not match
+ */
+ protected static <W extends Widget> W getWidget(TableCellElement e,
+ Class<W> klass) {
+ W w = WidgetUtil.findWidget(e.getFirstChildElement(), klass);
+ assert w == null || w.getElement() == e.getFirstChildElement() : "Widget not found inside cell";
+ return w;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java b/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java
index c08656c4d9..a2c54ec7ca 100644
--- a/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java
+++ b/client/src/com/vaadin/client/ui/AbstractClickEventHandler.java
@@ -32,7 +32,7 @@ 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.client.ComponentConnector;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
public abstract class AbstractClickEventHandler implements MouseDownHandler,
@@ -72,8 +72,8 @@ public abstract class AbstractClickEventHandler implements MouseDownHandler,
// Event's reported target not always correct if event
// capture is in use
- Element elementUnderMouse = Util.getElementUnderMouse(event
- .getNativeEvent());
+ Element elementUnderMouse = WidgetUtil
+ .getElementUnderMouse(event.getNativeEvent());
if (lastMouseDownTarget != null
&& elementUnderMouse == lastMouseDownTarget) {
mouseUpPreviewMatched = true;
@@ -171,7 +171,8 @@ public abstract class AbstractClickEventHandler implements MouseDownHandler,
* When getting a mousedown event, we must detect where the
* corresponding mouseup event if it's on a different part of the page.
*/
- lastMouseDownTarget = Util.getElementUnderMouse(event.getNativeEvent());
+ lastMouseDownTarget = WidgetUtil.getElementUnderMouse(event
+ .getNativeEvent());
mouseUpPreviewMatched = false;
mouseUpEventPreviewRegistration = Event
.addNativePreviewHandler(mouseUpPreviewHandler);
@@ -188,7 +189,7 @@ public abstract class AbstractClickEventHandler implements MouseDownHandler,
if (hasEventListener()
&& mouseUpPreviewMatched
&& lastMouseDownTarget != null
- && Util.getElementUnderMouse(event.getNativeEvent()) == lastMouseDownTarget
+ && WidgetUtil.getElementUnderMouse(event.getNativeEvent()) == lastMouseDownTarget
&& shouldFireEvent(event)) {
// "Click" with left, right or middle button
fireClick(event.getNativeEvent());
diff --git a/client/src/com/vaadin/client/ui/AbstractComponentConnector.java b/client/src/com/vaadin/client/ui/AbstractComponentConnector.java
index c3f14be40c..46ad289488 100644
--- a/client/src/com/vaadin/client/ui/AbstractComponentConnector.java
+++ b/client/src/com/vaadin/client/ui/AbstractComponentConnector.java
@@ -21,7 +21,6 @@ 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.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.HasComponentsConnector;
import com.vaadin.client.LayoutManager;
@@ -89,7 +88,7 @@ public abstract class AbstractComponentConnector extends AbstractConnector
} catch (NoDataException e) {
throw new IllegalStateException(
"Default implementation of createWidget() does not work for "
- + Util.getSimpleName(this)
+ + getClass().getSimpleName()
+ ". This might be caused by explicitely using "
+ "super.createWidget() or some unspecified "
+ "problem with the widgetset compilation.", e);
@@ -106,10 +105,10 @@ public abstract class AbstractComponentConnector extends AbstractConnector
public Widget getWidget() {
if (widget == null) {
Profiler.enter("AbstractComponentConnector.createWidget for "
- + Util.getSimpleName(this));
+ + getClass().getSimpleName());
widget = createWidget();
Profiler.leave("AbstractComponentConnector.createWidget for "
- + Util.getSimpleName(this));
+ + getClass().getSimpleName());
}
return widget;
@@ -195,8 +194,7 @@ public abstract class AbstractComponentConnector extends AbstractConnector
@Override
public void setWidgetEnabled(boolean widgetEnabled) {
// add or remove v-disabled style name from the widget
- setWidgetStyleName(ApplicationConnection.DISABLED_CLASSNAME,
- !widgetEnabled);
+ setWidgetStyleName(StyleConstants.DISABLED, !widgetEnabled);
if (getWidget() instanceof HasEnabled) {
// set widget specific enabled state
@@ -343,8 +341,7 @@ public abstract class AbstractComponentConnector extends AbstractConnector
// add / remove error style name
setWidgetStyleNameWithPrefix(primaryStyleName,
- ApplicationConnection.ERROR_CLASSNAME_EXT,
- null != state.errorMessage);
+ StyleConstants.ERROR_EXT, null != state.errorMessage);
// add additional user defined style names as class names, prefixed with
// component default class name. remove nonexistent style names.
diff --git a/client/src/com/vaadin/client/ui/AbstractConnector.java b/client/src/com/vaadin/client/ui/AbstractConnector.java
index e93ea0f507..a20c3463c2 100644
--- a/client/src/com/vaadin/client/ui/AbstractConnector.java
+++ b/client/src/com/vaadin/client/ui/AbstractConnector.java
@@ -120,11 +120,13 @@ public abstract class AbstractConnector implements ServerConnector,
addStateChangeHandler(this);
if (Profiler.isEnabled()) {
- Profiler.enter("AbstractConnector.init " + Util.getSimpleName(this));
+ Profiler.enter("AbstractConnector.init "
+ + getClass().getSimpleName());
}
init();
if (Profiler.isEnabled()) {
- Profiler.leave("AbstractConnector.init " + Util.getSimpleName(this));
+ Profiler.leave("AbstractConnector.init "
+ + getClass().getSimpleName());
}
Profiler.leave("AbstractConnector.doInit");
}
@@ -214,8 +216,8 @@ public abstract class AbstractConnector implements ServerConnector,
public void fireEvent(GwtEvent<?> event) {
String profilerKey = null;
if (Profiler.isEnabled()) {
- profilerKey = "Fire " + Util.getSimpleName(event) + " for "
- + Util.getSimpleName(this);
+ profilerKey = "Fire " + event.getClass().getSimpleName() + " for "
+ + getClass().getSimpleName();
Profiler.enter(profilerKey);
}
if (handlerManager != null) {
@@ -377,7 +379,7 @@ public abstract class AbstractConnector implements ServerConnector,
} catch (NoDataException e) {
throw new IllegalStateException(
"There is no information about the state for "
- + Util.getSimpleName(this)
+ + getClass().getSimpleName()
+ ". Did you remember to compile the right widgetset?",
e);
}
@@ -391,7 +393,7 @@ public abstract class AbstractConnector implements ServerConnector,
} catch (NoDataException e) {
throw new IllegalStateException(
"There is no information about the state for "
- + Util.getSimpleName(connector)
+ + connector.getClass().getSimpleName()
+ ". Did you remember to compile the right widgetset?",
e);
}
diff --git a/client/src/com/vaadin/client/ui/AbstractFieldConnector.java b/client/src/com/vaadin/client/ui/AbstractFieldConnector.java
index 965e79b6fd..8d8df81bd8 100644
--- a/client/src/com/vaadin/client/ui/AbstractFieldConnector.java
+++ b/client/src/com/vaadin/client/ui/AbstractFieldConnector.java
@@ -15,7 +15,7 @@
*/
package com.vaadin.client.ui;
-import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.StyleConstants;
import com.vaadin.shared.AbstractFieldState;
public abstract class AbstractFieldConnector extends AbstractComponentConnector {
@@ -51,14 +51,12 @@ public abstract class AbstractFieldConnector extends AbstractComponentConnector
super.updateWidgetStyleNames();
// add / remove modified style name to Fields
- setWidgetStyleName(ApplicationConnection.MODIFIED_CLASSNAME,
- isModified());
+ setWidgetStyleName(StyleConstants.MODIFIED, isModified());
// add / remove error style name to Fields
setWidgetStyleNameWithPrefix(getWidget().getStylePrimaryName(),
- ApplicationConnection.REQUIRED_CLASSNAME_EXT, isRequired());
+ StyleConstants.REQUIRED_EXT, isRequired());
- getWidget().setStyleName(ApplicationConnection.REQUIRED_CLASSNAME,
- isRequired());
+ getWidget().setStyleName(StyleConstants.REQUIRED, isRequired());
}
}
diff --git a/client/src/com/vaadin/client/ui/MediaBaseConnector.java b/client/src/com/vaadin/client/ui/MediaBaseConnector.java
index cebb2e3836..fdd9610517 100644
--- a/client/src/com/vaadin/client/ui/MediaBaseConnector.java
+++ b/client/src/com/vaadin/client/ui/MediaBaseConnector.java
@@ -15,7 +15,7 @@
*/
package com.vaadin.client.ui;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.shared.communication.URLReference;
import com.vaadin.shared.ui.AbstractMediaState;
@@ -78,7 +78,7 @@ public abstract class MediaBaseConnector extends AbstractComponentConnector {
if (altText == null || "".equals(altText)) {
altText = getDefaultAltHtml();
} else if (!getState().htmlContentAllowed) {
- altText = Util.escapeHTML(altText);
+ altText = WidgetUtil.escapeHTML(altText);
}
getWidget().setAltText(altText);
}
diff --git a/client/src/com/vaadin/client/ui/SubPartAware.java b/client/src/com/vaadin/client/ui/SubPartAware.java
index a064b8a8a8..7a40eea20e 100644
--- a/client/src/com/vaadin/client/ui/SubPartAware.java
+++ b/client/src/com/vaadin/client/ui/SubPartAware.java
@@ -16,11 +16,10 @@
package com.vaadin.client.ui;
import com.google.gwt.user.client.ui.Widget;
-import com.vaadin.client.componentlocator.ComponentLocator;
/**
* Interface implemented by {@link Widget}s which can provide identifiers for at
- * least one element inside the component. Used by {@link ComponentLocator}.
+ * least one element inside the component.
*
*/
public interface SubPartAware {
diff --git a/client/src/com/vaadin/client/ui/VAbstractSplitPanel.java b/client/src/com/vaadin/client/ui/VAbstractSplitPanel.java
index 9d32355b70..b52663b161 100644
--- a/client/src/com/vaadin/client/ui/VAbstractSplitPanel.java
+++ b/client/src/com/vaadin/client/ui/VAbstractSplitPanel.java
@@ -44,7 +44,7 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.StyleConstants;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler;
import com.vaadin.client.ui.VAbstractSplitPanel.SplitterMoveHandler.SplitterMoveEvent;
@@ -577,7 +577,7 @@ public class VAbstractSplitPanel extends ComplexPanel {
break;
}
// Only fire click event listeners if the splitter isn't moved
- if (Util.isTouchEvent(event) || !resized) {
+ if (WidgetUtil.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
@@ -596,8 +596,8 @@ public class VAbstractSplitPanel extends ComplexPanel {
DOM.setCapture(getElement());
origX = DOM.getElementPropertyInt(splitter, "offsetLeft");
origY = DOM.getElementPropertyInt(splitter, "offsetTop");
- origMouseX = Util.getTouchOrMouseClientX(event);
- origMouseY = Util.getTouchOrMouseClientY(event);
+ origMouseX = WidgetUtil.getTouchOrMouseClientX(event);
+ origMouseY = WidgetUtil.getTouchOrMouseClientY(event);
event.stopPropagation();
event.preventDefault();
}
@@ -606,12 +606,12 @@ public class VAbstractSplitPanel extends ComplexPanel {
public void onMouseMove(Event event) {
switch (orientation) {
case HORIZONTAL:
- final int x = Util.getTouchOrMouseClientX(event);
+ final int x = WidgetUtil.getTouchOrMouseClientX(event);
onHorizontalMouseMove(x);
break;
case VERTICAL:
default:
- final int y = Util.getTouchOrMouseClientY(event);
+ final int y = WidgetUtil.getTouchOrMouseClientY(event);
onVerticalMouseMove(y);
break;
}
@@ -688,7 +688,7 @@ public class VAbstractSplitPanel extends ComplexPanel {
DOM.releaseCapture(getElement());
hideDraggingCurtain();
resizing = false;
- if (!Util.isTouchEvent(event)) {
+ if (!WidgetUtil.isTouchEvent(event)) {
onMouseMove(event);
}
fireEvent(new SplitterMoveEvent(this));
diff --git a/client/src/com/vaadin/client/ui/VAccordion.java b/client/src/com/vaadin/client/ui/VAccordion.java
index 422f195af9..06eaecaf70 100644
--- a/client/src/com/vaadin/client/ui/VAccordion.java
+++ b/client/src/com/vaadin/client/ui/VAccordion.java
@@ -29,7 +29,7 @@ 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.client.ComponentConnector;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VCaption;
import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler;
import com.vaadin.shared.ComponentConstants;
@@ -203,7 +203,7 @@ public class VAccordion extends VTabsheetBase {
}
int captionWidth = caption.getRequiredWidth();
- int padding = Util.measureHorizontalPaddingAndBorder(
+ int padding = WidgetUtil.measureHorizontalPaddingAndBorder(
caption.getElement(), 18);
return captionWidth + padding;
}
diff --git a/client/src/com/vaadin/client/ui/VButton.java b/client/src/com/vaadin/client/ui/VButton.java
index dcc364c1da..bf321f7f00 100644
--- a/client/src/com/vaadin/client/ui/VButton.java
+++ b/client/src/com/vaadin/client/ui/VButton.java
@@ -30,6 +30,7 @@ import com.google.gwt.user.client.ui.FocusWidget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
public class VButton extends FocusWidget implements ClickHandler {
@@ -373,10 +374,10 @@ public class VButton extends FocusWidget implements ClickHandler {
// Set (x,y) client coordinates to the middle of the button
int x = getElement().getAbsoluteLeft() - getElement().getScrollLeft()
- getElement().getOwnerDocument().getScrollLeft()
- + Util.getRequiredWidth(getElement()) / 2;
+ + WidgetUtil.getRequiredWidth(getElement()) / 2;
int y = getElement().getAbsoluteTop() - getElement().getScrollTop()
- getElement().getOwnerDocument().getScrollTop()
- + Util.getRequiredHeight(getElement()) / 2;
+ + WidgetUtil.getRequiredHeight(getElement()) / 2;
NativeEvent evt = Document.get().createClickEvent(1, 0, 0, x, y, false,
false, false, false);
getElement().dispatchEvent(evt);
diff --git a/client/src/com/vaadin/client/ui/VCalendarPanel.java b/client/src/com/vaadin/client/ui/VCalendarPanel.java
index 6fc06bb153..e1b906b6e4 100644
--- a/client/src/com/vaadin/client/ui/VCalendarPanel.java
+++ b/client/src/com/vaadin/client/ui/VCalendarPanel.java
@@ -54,7 +54,7 @@ import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.DateTimeService;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
import com.vaadin.shared.ui.datefield.Resolution;
import com.vaadin.shared.util.SharedUtil;
@@ -2065,7 +2065,7 @@ public class VCalendarPanel extends FocusableFlexTable implements
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);
+ Day day = WidgetUtil.findWidget(subElement, Day.class);
if (day != null) {
Date date = day.getDate();
int id = date.getDate();
diff --git a/client/src/com/vaadin/client/ui/VContextMenu.java b/client/src/com/vaadin/client/ui/VContextMenu.java
index fa6d67fc0c..6028eea52c 100644
--- a/client/src/com/vaadin/client/ui/VContextMenu.java
+++ b/client/src/com/vaadin/client/ui/VContextMenu.java
@@ -51,7 +51,7 @@ import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.impl.FocusImpl;
import com.vaadin.client.Focusable;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
public class VContextMenu extends VOverlay implements SubPartAware {
@@ -89,7 +89,7 @@ public class VContextMenu extends VOverlay implements SubPartAware {
addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
- Element currentFocus = Util.getFocusedElement();
+ Element currentFocus = WidgetUtil.getFocusedElement();
if (focusedElement != null
&& (currentFocus == null
|| menu.getElement().isOrHasChild(currentFocus) || RootPanel
@@ -137,11 +137,11 @@ public class VContextMenu extends VOverlay implements SubPartAware {
}
// Attach onload listeners to all images
- Util.sinkOnloadForImages(menu.getElement());
+ WidgetUtil.sinkOnloadForImages(menu.getElement());
// Store the currently focused element, which will be re-focused when
// context menu is closed
- focusedElement = Util.getFocusedElement();
+ focusedElement = WidgetUtil.getFocusedElement();
// reset height (if it has been previously set explicitly)
setHeight("");
diff --git a/client/src/com/vaadin/client/ui/VCustomLayout.java b/client/src/com/vaadin/client/ui/VCustomLayout.java
index f5d572007a..5f8a8197d0 100644
--- a/client/src/com/vaadin/client/ui/VCustomLayout.java
+++ b/client/src/com/vaadin/client/ui/VCustomLayout.java
@@ -37,6 +37,7 @@ import com.vaadin.client.StyleConstants;
import com.vaadin.client.Util;
import com.vaadin.client.VCaption;
import com.vaadin.client.VCaptionWrapper;
+import com.vaadin.client.WidgetUtil;
/**
* Custom Layout implements complex layout defined with HTML template.
@@ -158,7 +159,8 @@ public class VCustomLayout extends ComplexPanel {
// TODO prefix img src:s here with a regeps, cannot work further with IE
- String relImgPrefix = Util.escapeAttribute(themeUri + "/layouts/");
+ String relImgPrefix = WidgetUtil
+ .escapeAttribute(themeUri + "/layouts/");
// prefix all relative image elements to point to theme dir with a
// regexp search
diff --git a/client/src/com/vaadin/client/ui/VEmbedded.java b/client/src/com/vaadin/client/ui/VEmbedded.java
index acf814471a..f3970f9ac7 100644
--- a/client/src/com/vaadin/client/ui/VEmbedded.java
+++ b/client/src/com/vaadin/client/ui/VEmbedded.java
@@ -31,6 +31,7 @@ import com.vaadin.client.ConnectorMap;
import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.embedded.EmbeddedConstants;
public class VEmbedded extends HTML {
@@ -83,8 +84,8 @@ public class VEmbedded extends HTML {
*/
if (uidl.hasAttribute("classid")) {
html.append("classid=\""
- + Util.escapeAttribute(uidl.getStringAttribute("classid"))
- + "\" ");
+ + WidgetUtil.escapeAttribute(uidl
+ .getStringAttribute("classid")) + "\" ");
} else {
html.append("classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" ");
}
@@ -99,8 +100,8 @@ public class VEmbedded extends HTML {
*/
if (uidl.hasAttribute("codebase")) {
html.append("codebase=\""
- + Util.escapeAttribute(uidl.getStringAttribute("codebase"))
- + "\" ");
+ + WidgetUtil.escapeAttribute(uidl
+ .getStringAttribute("codebase")) + "\" ");
} else {
html.append("codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0\" ");
}
@@ -111,29 +112,29 @@ public class VEmbedded extends HTML {
String width = paintable.getState().width;
// Add width and height
- html.append("width=\"" + Util.escapeAttribute(width) + "\" ");
- html.append("height=\"" + Util.escapeAttribute(height) + "\" ");
+ html.append("width=\"" + WidgetUtil.escapeAttribute(width) + "\" ");
+ html.append("height=\"" + WidgetUtil.escapeAttribute(height) + "\" ");
html.append("type=\"application/x-shockwave-flash\" ");
// Codetype
if (uidl.hasAttribute("codetype")) {
html.append("codetype=\""
- + Util.escapeAttribute(uidl.getStringAttribute("codetype"))
- + "\" ");
+ + WidgetUtil.escapeAttribute(uidl
+ .getStringAttribute("codetype")) + "\" ");
}
// Standby
if (uidl.hasAttribute("standby")) {
html.append("standby=\""
- + Util.escapeAttribute(uidl.getStringAttribute("standby"))
- + "\" ");
+ + WidgetUtil.escapeAttribute(uidl
+ .getStringAttribute("standby")) + "\" ");
}
// Archive
if (uidl.hasAttribute("archive")) {
html.append("archive=\""
- + Util.escapeAttribute(uidl.getStringAttribute("archive"))
- + "\" ");
+ + WidgetUtil.escapeAttribute(uidl
+ .getStringAttribute("archive")) + "\" ");
}
// End object tag
@@ -148,25 +149,25 @@ public class VEmbedded extends HTML {
// 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("name=\"" + WidgetUtil.escapeAttribute(name) + "\" ");
+ html.append("value=\""
+ + WidgetUtil.escapeAttribute(parameters.get(name)) + "\" ");
html.append("/>");
}
// Build inner EMBED tag
html.append("<embed ");
- html.append("src=\"" + Util.escapeAttribute(getSrc(uidl, client))
+ html.append("src=\"" + WidgetUtil.escapeAttribute(getSrc(uidl, client))
+ "\" ");
- html.append("width=\"" + Util.escapeAttribute(width) + "\" ");
- html.append("height=\"" + Util.escapeAttribute(height) + "\" ");
+ html.append("width=\"" + WidgetUtil.escapeAttribute(width) + "\" ");
+ html.append("height=\"" + WidgetUtil.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(WidgetUtil.escapeAttribute(name));
html.append("=");
- html.append("\"" + Util.escapeAttribute(parameters.get(name))
+ html.append("\"" + WidgetUtil.escapeAttribute(parameters.get(name))
+ "\"");
}
diff --git a/client/src/com/vaadin/client/ui/VFilterSelect.java b/client/src/com/vaadin/client/ui/VFilterSelect.java
index bb217f2de2..c0575b1ea5 100644
--- a/client/src/com/vaadin/client/ui/VFilterSelect.java
+++ b/client/src/com/vaadin/client/ui/VFilterSelect.java
@@ -67,8 +67,8 @@ import com.vaadin.client.ComputedStyle;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.Focusable;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.aria.AriaHelper;
import com.vaadin.client.ui.aria.HandlesAriaCaption;
import com.vaadin.client.ui.aria.HandlesAriaInvalid;
@@ -134,7 +134,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
// options and are not collapsed (#7506)
content = "&nbsp;";
} else {
- content = Util.escapeHTML(caption);
+ content = WidgetUtil.escapeHTML(caption);
}
sb.append("<span>" + content + "</span>");
return sb.toString();
@@ -599,8 +599,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
final int naturalMenuWidth = menuFirstChild.getOffsetWidth();
if (popupOuterPadding == -1) {
- popupOuterPadding = Util.measureHorizontalPaddingAndBorder(
- getElement(), 2);
+ popupOuterPadding = WidgetUtil
+ .measureHorizontalPaddingAndBorder(getElement(), 2);
}
if (naturalMenuWidth < desiredWidth) {
@@ -657,7 +657,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
menu.setHeight(menuHeight + "px");
final int naturalMenuWidthPlusScrollBar = naturalMenuWidth
- + Util.getNativeScrollbarSize();
+ + WidgetUtil.getNativeScrollbarSize();
if (offsetWidth < naturalMenuWidthPlusScrollBar) {
menu.setWidth(naturalMenuWidthPlusScrollBar + "px");
}
@@ -818,7 +818,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
Roles.getListitemRole().set(mi.getElement());
- Util.sinkOnloadForImages(mi.getElement());
+ WidgetUtil.sinkOnloadForImages(mi.getElement());
this.addItem(mi);
if (s == currentSuggestion) {
@@ -1069,7 +1069,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
* the end and the focus to the start. This makes Firefox work
* the same way as other browsers (#13477)
*/
- Util.setSelectionRange(getElement(), pos, length, "backward");
+ WidgetUtil.setSelectionRange(getElement(), pos, length,
+ "backward");
} else {
/*
@@ -1609,7 +1610,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
}
private void forceReflow() {
- Util.setStyleTemporarily(tb.getElement(), "zoom", "1");
+ WidgetUtil.setStyleTemporarily(tb.getElement(), "zoom", "1");
}
/**
@@ -1621,7 +1622,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
int availableHeight = 0;
availableHeight = getOffsetHeight();
- int iconHeight = Util.getRequiredHeight(selectedItemIcon);
+ int iconHeight = WidgetUtil.getRequiredHeight(selectedItemIcon);
int marginTop = (availableHeight - iconHeight) / 2;
selectedItemIcon.getElement().getStyle()
.setMarginTop(marginTop, Unit.PX);
@@ -1936,7 +1937,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
*/
public void updateSuggestionPopupMinWidth() {
// used only to calculate minimum width
- String captions = Util.escapeHTML(inputPrompt);
+ String captions = WidgetUtil.escapeHTML(inputPrompt);
for (FilterSelectSuggestion suggestion : currentSuggestions) {
// Collect captions so we can calculate minimum width for
@@ -1944,7 +1945,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
if (captions.length() > 0) {
captions += "|";
}
- captions += Util.escapeHTML(suggestion.getReplacementString());
+ captions += WidgetUtil
+ .escapeHTML(suggestion.getReplacementString());
}
// Calculate minimum textarea width
@@ -2051,7 +2053,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
*/
preventNextBlurEventInIE = false;
- Element focusedElement = Util.getIEFocusedElement();
+ Element focusedElement = WidgetUtil.getFocusedElement();
if (getElement().isOrHasChild(focusedElement)
|| suggestionPopup.getElement()
.isOrHasChild(focusedElement)) {
@@ -2129,7 +2131,7 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
* when the popup is used to view longer items than the text box is
* wide.
*/
- int w = Util.getRequiredWidth(this);
+ int w = WidgetUtil.getRequiredWidth(this);
if ((!initDone || currentPage + 1 < 0)
&& suggestionPopupMinWidth > w) {
@@ -2150,9 +2152,9 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler,
// Use util.getRequiredWidth instead of getOffsetWidth here
- int iconWidth = selectedItemIcon == null ? 0 : Util
+ int iconWidth = selectedItemIcon == null ? 0 : WidgetUtil
.getRequiredWidth(selectedItemIcon);
- int buttonWidth = popupOpener == null ? 0 : Util
+ int buttonWidth = popupOpener == null ? 0 : WidgetUtil
.getRequiredWidth(popupOpener);
/*
diff --git a/client/src/com/vaadin/client/ui/VFlash.java b/client/src/com/vaadin/client/ui/VFlash.java
index cf15f89cb4..eaf53836ee 100644
--- a/client/src/com/vaadin/client/ui/VFlash.java
+++ b/client/src/com/vaadin/client/ui/VFlash.java
@@ -19,7 +19,7 @@ import java.util.HashMap;
import java.util.Map;
import com.google.gwt.user.client.ui.HTML;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
public class VFlash extends HTML {
@@ -156,7 +156,8 @@ public class VFlash extends HTML {
* this by setting his own classid.
*/
if (classId != null) {
- html.append("classid=\"" + Util.escapeAttribute(classId) + "\" ");
+ html.append("classid=\"" + WidgetUtil.escapeAttribute(classId)
+ + "\" ");
} else {
html.append("classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" ");
}
@@ -170,29 +171,33 @@ public class VFlash extends HTML {
* codebase
*/
if (codebase != null) {
- html.append("codebase=\"" + Util.escapeAttribute(codebase) + "\" ");
+ html.append("codebase=\"" + WidgetUtil.escapeAttribute(codebase)
+ + "\" ");
} else {
html.append("codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0\" ");
}
// Add width and height
- html.append("width=\"" + Util.escapeAttribute(width) + "\" ");
- html.append("height=\"" + Util.escapeAttribute(height) + "\" ");
+ html.append("width=\"" + WidgetUtil.escapeAttribute(width) + "\" ");
+ html.append("height=\"" + WidgetUtil.escapeAttribute(height) + "\" ");
html.append("type=\"application/x-shockwave-flash\" ");
// Codetype
if (codetype != null) {
- html.append("codetype=\"" + Util.escapeAttribute(codetype) + "\" ");
+ html.append("codetype=\"" + WidgetUtil.escapeAttribute(codetype)
+ + "\" ");
}
// Standby
if (standby != null) {
- html.append("standby=\"" + Util.escapeAttribute(standby) + "\" ");
+ html.append("standby=\"" + WidgetUtil.escapeAttribute(standby)
+ + "\" ");
}
// Archive
if (archive != null) {
- html.append("archive=\"" + Util.escapeAttribute(archive) + "\" ");
+ html.append("archive=\"" + WidgetUtil.escapeAttribute(archive)
+ + "\" ");
}
// End object tag
@@ -206,25 +211,25 @@ public class VFlash extends HTML {
// Add parameters to OBJECT
for (String name : embedParams.keySet()) {
html.append("<param ");
- html.append("name=\"" + Util.escapeAttribute(name) + "\" ");
+ html.append("name=\"" + WidgetUtil.escapeAttribute(name) + "\" ");
html.append("value=\""
- + Util.escapeAttribute(embedParams.get(name)) + "\" ");
+ + WidgetUtil.escapeAttribute(embedParams.get(name)) + "\" ");
html.append("/>");
}
// Build inner EMBED tag
html.append("<embed ");
- html.append("src=\"" + Util.escapeAttribute(source) + "\" ");
- html.append("width=\"" + Util.escapeAttribute(width) + "\" ");
- html.append("height=\"" + Util.escapeAttribute(height) + "\" ");
+ html.append("src=\"" + WidgetUtil.escapeAttribute(source) + "\" ");
+ html.append("width=\"" + WidgetUtil.escapeAttribute(width) + "\" ");
+ html.append("height=\"" + WidgetUtil.escapeAttribute(height) + "\" ");
html.append("type=\"application/x-shockwave-flash\" ");
// Add the parameters to the Embed
for (String name : embedParams.keySet()) {
- html.append(Util.escapeAttribute(name));
+ html.append(WidgetUtil.escapeAttribute(name));
html.append("=");
- html.append("\"" + Util.escapeAttribute(embedParams.get(name))
- + "\"");
+ html.append("\""
+ + WidgetUtil.escapeAttribute(embedParams.get(name)) + "\"");
}
// End embed tag
diff --git a/client/src/com/vaadin/client/ui/VFormLayout.java b/client/src/com/vaadin/client/ui/VFormLayout.java
index 64a7c5e579..a2ea77d31c 100644
--- a/client/src/com/vaadin/client/ui/VFormLayout.java
+++ b/client/src/com/vaadin/client/ui/VFormLayout.java
@@ -30,7 +30,6 @@ 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.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.Focusable;
@@ -78,7 +77,7 @@ public class VFormLayout extends SimplePanel {
}
if (!enabled) {
- styles.add(ApplicationConnection.DISABLED_CLASSNAME);
+ styles.add(StyleConstants.DISABLED);
}
return styles.toArray(new String[styles.size()]);
@@ -242,7 +241,7 @@ public class VFormLayout extends SimplePanel {
if (styles != null) {
for (String style : styles) {
- if (ApplicationConnection.DISABLED_CLASSNAME.equals(style)) {
+ if (StyleConstants.DISABLED.equals(style)) {
// Add v-disabled also without classname prefix so
// generic v-disabled CSS rules work
styleName += " " + style;
diff --git a/client/src/com/vaadin/client/ui/VLabel.java b/client/src/com/vaadin/client/ui/VLabel.java
index 0f996fa6b9..a3572759c4 100644
--- a/client/src/com/vaadin/client/ui/VLabel.java
+++ b/client/src/com/vaadin/client/ui/VLabel.java
@@ -21,6 +21,7 @@ import com.google.gwt.user.client.ui.HTML;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.Util;
import com.vaadin.client.VTooltip;
+import com.vaadin.client.WidgetUtil;
public class VLabel extends HTML {
@@ -63,7 +64,7 @@ public class VLabel extends HTML {
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));
+ super.setHTML(WidgetUtil.escapeHTML(text));
} else {
super.setText(text);
}
diff --git a/client/src/com/vaadin/client/ui/VMenuBar.java b/client/src/com/vaadin/client/ui/VMenuBar.java
index b5dac3f7ef..08f70f4dde 100644
--- a/client/src/com/vaadin/client/ui/VMenuBar.java
+++ b/client/src/com/vaadin/client/ui/VMenuBar.java
@@ -50,6 +50,7 @@ import com.vaadin.client.LayoutManager;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.menubar.MenuBarConstants;
public class VMenuBar extends SimpleFocusablePanel implements
@@ -234,7 +235,7 @@ public class VMenuBar extends SimpleFocusablePanel implements
}
String itemText = item.getStringAttribute("text");
if (!htmlContentAllowed) {
- itemText = Util.escapeHTML(itemText);
+ itemText = WidgetUtil.escapeHTML(itemText);
}
itemHTML.append(itemText);
itemHTML.append("</span>");
@@ -658,7 +659,8 @@ public class VMenuBar extends SimpleFocusablePanel implements
// Make room for the scroll bar by adjusting the width of the
// popup
- style.setWidth(contentWidth + Util.getNativeScrollbarSize(),
+ style.setWidth(
+ contentWidth + WidgetUtil.getNativeScrollbarSize(),
Unit.PX);
popup.positionOrSizeUpdated();
}
@@ -983,7 +985,7 @@ public class VMenuBar extends SimpleFocusablePanel implements
// Sink the onload event for any icons. The onload
// events are handled by the parent VMenuBar.
- Util.sinkOnloadForImages(getElement());
+ WidgetUtil.sinkOnloadForImages(getElement());
}
@Override
@@ -993,7 +995,7 @@ public class VMenuBar extends SimpleFocusablePanel implements
@Override
public void setText(String text) {
- setHTML(Util.escapeHTML(text));
+ setHTML(WidgetUtil.escapeHTML(text));
}
public void setEnabled(boolean enabled) {
diff --git a/client/src/com/vaadin/client/ui/VNativeButton.java b/client/src/com/vaadin/client/ui/VNativeButton.java
index 8e0dd2bce1..77b2515f45 100644
--- a/client/src/com/vaadin/client/ui/VNativeButton.java
+++ b/client/src/com/vaadin/client/ui/VNativeButton.java
@@ -25,6 +25,7 @@ import com.google.gwt.user.client.ui.Button;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.MouseEventDetailsBuilder;
+import com.vaadin.client.StyleConstants;
import com.vaadin.client.Util;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.button.ButtonServerRpc;
@@ -146,7 +147,7 @@ public class VNativeButton extends Button implements ClickHandler {
setEnabled(false);
// FIXME: This should be moved to NativeButtonConnector along with
// buttonRpcProxy
- addStyleName(ApplicationConnection.DISABLED_CLASSNAME);
+ addStyleName(StyleConstants.DISABLED);
buttonRpcProxy.disableOnClick();
}
diff --git a/client/src/com/vaadin/client/ui/VNotification.java b/client/src/com/vaadin/client/ui/VNotification.java
index 5e1df67e18..d7639b0022 100644
--- a/client/src/com/vaadin/client/ui/VNotification.java
+++ b/client/src/com/vaadin/client/ui/VNotification.java
@@ -38,7 +38,7 @@ import com.vaadin.client.AnimationUtil.AnimationEndListener;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.aria.AriaHelper;
import com.vaadin.shared.Position;
import com.vaadin.shared.ui.ui.NotificationRole;
@@ -259,7 +259,7 @@ public class VNotification extends VOverlay {
* nudge (#8551)
*/
if (BrowserInfo.get().isAndroid()) {
- Util.setStyleTemporarily(getElement(), "display", "none");
+ WidgetUtil.setStyleTemporarily(getElement(), "display", "none");
}
}
@@ -491,7 +491,7 @@ public class VNotification extends VOverlay {
String caption = notification
.getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_CAPTION);
if (onlyPlainText) {
- caption = Util.escapeHTML(caption);
+ caption = WidgetUtil.escapeHTML(caption);
caption = caption.replaceAll("\\n", "<br />");
}
html += "<h1>" + caption + "</h1>";
@@ -501,7 +501,7 @@ public class VNotification extends VOverlay {
String message = notification
.getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_MESSAGE);
if (onlyPlainText) {
- message = Util.escapeHTML(message);
+ message = WidgetUtil.escapeHTML(message);
message = message.replaceAll("\\n", "<br />");
}
html += "<p>" + message + "</p>";
diff --git a/client/src/com/vaadin/client/ui/VOptionGroup.java b/client/src/com/vaadin/client/ui/VOptionGroup.java
index 34227831b9..d429752069 100644
--- a/client/src/com/vaadin/client/ui/VOptionGroup.java
+++ b/client/src/com/vaadin/client/ui/VOptionGroup.java
@@ -40,10 +40,11 @@ import com.google.gwt.user.client.ui.HasEnabled;
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.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.StyleConstants;
import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.EventId;
import com.vaadin.shared.ui.optiongroup.OptionGroupConstants;
@@ -136,7 +137,7 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
String itemHtml = opUidl.getStringAttribute("caption");
if (!htmlContentAllowed) {
- itemHtml = Util.escapeHTML(itemHtml);
+ itemHtml = WidgetUtil.escapeHTML(itemHtml);
}
String iconUrl = opUidl.getStringAttribute("icon");
@@ -160,7 +161,7 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
op.setStyleName("v-radiobutton");
}
if (iconUrl != null && iconUrl.length() != 0) {
- Util.sinkOnloadForImages(op.getElement());
+ WidgetUtil.sinkOnloadForImages(op.getElement());
op.addHandler(iconLoadHandler, LoadEvent.getType());
}
@@ -178,8 +179,7 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
op.setEnabled(enabled);
optionsEnabled.put(op, optionEnabled);
- setStyleName(op.getElement(),
- ApplicationConnection.DISABLED_CLASSNAME,
+ setStyleName(op.getElement(), StyleConstants.DISABLED,
!(optionEnabled && isEnabled()));
newwidgets.add(op);
@@ -248,14 +248,12 @@ public class VOptionGroup extends VOptionGroupBase implements FocusHandler,
Boolean isOptionEnabled = optionsEnabled.get(w);
if (isOptionEnabled == null) {
hasEnabled.setEnabled(optionGroupEnabled);
- setStyleName(w.getElement(),
- ApplicationConnection.DISABLED_CLASSNAME,
+ setStyleName(w.getElement(), StyleConstants.DISABLED,
!isEnabled());
} else {
hasEnabled
.setEnabled(isOptionEnabled && optionGroupEnabled);
- setStyleName(w.getElement(),
- ApplicationConnection.DISABLED_CLASSNAME,
+ setStyleName(w.getElement(), StyleConstants.DISABLED,
!(isOptionEnabled && isEnabled()));
}
}
diff --git a/client/src/com/vaadin/client/ui/VOverlay.java b/client/src/com/vaadin/client/ui/VOverlay.java
index 9845e89dab..3649afc74f 100644
--- a/client/src/com/vaadin/client/ui/VOverlay.java
+++ b/client/src/com/vaadin/client/ui/VOverlay.java
@@ -44,6 +44,7 @@ import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ComputedStyle;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
/**
* <p>
@@ -672,7 +673,7 @@ public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> {
// IE9 and IE10 have a bug, when resize an a element with box-shadow.
// IE9 and IE10 need explicit update to remove extra box-shadows
if (BrowserInfo.get().isIE9() || BrowserInfo.get().isIE10()) {
- Util.forceIERedraw(getElement());
+ WidgetUtil.forceIERedraw(getElement());
}
}
diff --git a/client/src/com/vaadin/client/ui/VPopupView.java b/client/src/com/vaadin/client/ui/VPopupView.java
index 1923fc55e6..0f4e68acab 100644
--- a/client/src/com/vaadin/client/ui/VPopupView.java
+++ b/client/src/com/vaadin/client/ui/VPopupView.java
@@ -33,7 +33,14 @@ import com.google.gwt.event.logical.shared.CloseHandler;
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.*;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HasEnabled;
+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.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.DeferredWorker;
diff --git a/client/src/com/vaadin/client/ui/VProgressBar.java b/client/src/com/vaadin/client/ui/VProgressBar.java
index 8d23d0c36d..348791728f 100644
--- a/client/src/com/vaadin/client/ui/VProgressBar.java
+++ b/client/src/com/vaadin/client/ui/VProgressBar.java
@@ -21,8 +21,7 @@ import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.Widget;
-import com.vaadin.client.ApplicationConnection;
-import com.vaadin.shared.ui.progressindicator.ProgressBarState;
+import com.vaadin.client.StyleConstants;
/**
* Widget for showing the current progress of a long running task.
@@ -37,6 +36,8 @@ import com.vaadin.shared.ui.progressindicator.ProgressBarState;
*/
public class VProgressBar extends Widget implements HasEnabled {
+ public static final String PRIMARY_STYLE_NAME = "v-progressbar";
+
Element wrapper = DOM.createDiv();
Element indicator = DOM.createDiv();
@@ -49,7 +50,7 @@ public class VProgressBar extends Widget implements HasEnabled {
getElement().appendChild(wrapper);
wrapper.appendChild(indicator);
- setStylePrimaryName(ProgressBarState.PRIMARY_STYLE_NAME);
+ setStylePrimaryName(PRIMARY_STYLE_NAME);
}
/*
@@ -92,8 +93,9 @@ public class VProgressBar extends Widget implements HasEnabled {
@Override
public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- setStyleName(ApplicationConnection.DISABLED_CLASSNAME, !enabled);
+ if (this.enabled != enabled) {
+ this.enabled = enabled;
+ setStyleName(StyleConstants.DISABLED, !enabled);
+ }
}
-
}
diff --git a/client/src/com/vaadin/client/ui/VScrollTable.java b/client/src/com/vaadin/client/ui/VScrollTable.java
index 9a70653a15..927e2c31db 100644
--- a/client/src/com/vaadin/client/ui/VScrollTable.java
+++ b/client/src/com/vaadin/client/ui/VScrollTable.java
@@ -91,6 +91,7 @@ import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
import com.vaadin.client.VConsole;
import com.vaadin.client.VTooltip;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow;
import com.vaadin.client.ui.dd.DDUtil;
import com.vaadin.client.ui.dd.VAbstractDropHandler;
@@ -510,8 +511,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
@Override
public void showContextMenu(Event event) {
- int left = Util.getTouchOrMouseClientX(event);
- int top = Util.getTouchOrMouseClientY(event);
+ int left = WidgetUtil.getTouchOrMouseClientX(event);
+ int top = WidgetUtil.getTouchOrMouseClientY(event);
boolean menuShown = handleBodyContextMenu(left, top);
if (menuShown) {
event.stopPropagation();
@@ -796,8 +797,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
// Event's reported target not always correct if event
// capture is in use
- Element elementUnderMouse = Util.getElementUnderMouse(event
- .getNativeEvent());
+ Element elementUnderMouse = WidgetUtil
+ .getElementUnderMouse(event.getNativeEvent());
if (lastMouseDownTarget != null
&& lastMouseDownTarget.isOrHasChild(elementUnderMouse)) {
mouseUpPreviewMatched = true;
@@ -2253,7 +2254,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
int w = total;
w += scrollBody.getCellExtraWidth() * visibleColOrder.length;
if (willHaveScrollbarz) {
- w += Util.getNativeScrollbarSize();
+ w += WidgetUtil.getNativeScrollbarSize();
}
setContentWidth(w);
}
@@ -2266,7 +2267,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length;
if (willHaveScrollbarz) {
- availW -= Util.getNativeScrollbarSize();
+ availW -= WidgetUtil.getNativeScrollbarSize();
}
// TODO refactor this code to be the same as in resize timer
@@ -2438,10 +2439,10 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
}
boolean needsSpaceForHorizontalSrollbar = (total > availW);
if (needsSpaceForHorizontalSrollbar) {
- bodyHeight += Util.getNativeScrollbarSize();
+ bodyHeight += WidgetUtil.getNativeScrollbarSize();
}
scrollBodyPanel.setHeight(bodyHeight + "px");
- Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
+ WidgetUtil.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
}
isNewBody = false;
@@ -2472,7 +2473,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
* Ensures the column alignments are correct at initial loading. <br/>
* (child components widths are correct)
*/
- Util.runWebkitOverflowAutoFixDeferred(scrollBodyPanel.getElement());
+ WidgetUtil.runWebkitOverflowAutoFixDeferred(scrollBodyPanel
+ .getElement());
hadScrollBars = willHaveScrollbarz;
}
@@ -3131,7 +3133,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
case Event.ONTOUCHSTART:
case Event.ONMOUSEDOWN:
if (columnReordering
- && Util.isTouchEventOrLeftMouseButton(event)) {
+ && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
if (event.getTypeInt() == Event.ONTOUCHSTART) {
/*
* prevent using this event in e.g. scrolling
@@ -3151,11 +3153,11 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
case Event.ONTOUCHEND:
case Event.ONTOUCHCANCEL:
if (columnReordering
- && Util.isTouchEventOrLeftMouseButton(event)) {
+ && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
dragging = false;
DOM.releaseCapture(getElement());
- if (Util.isTouchEvent(event)) {
+ if (WidgetUtil.isTouchEvent(event)) {
/*
* Prevent using in e.g. scrolling and prevent generated
* events.
@@ -3181,7 +3183,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
if (!moved) {
// mouse event was a click to header -> sort column
- if (sortable && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (sortable
+ && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
if (sortColumn.equals(cid)) {
// just toggle order
client.updateVariable(paintableId, "sortascending",
@@ -3203,7 +3206,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
rowRequestHandler.run(); // run immediately
}
fireHeaderClickedEvent(event);
- if (Util.isTouchEvent(event)) {
+ if (WidgetUtil.isTouchEvent(event)) {
/*
* Prevent using in e.g. scrolling and prevent generated
* events.
@@ -3219,7 +3222,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
break;
case Event.ONTOUCHMOVE:
case Event.ONMOUSEMOVE:
- if (dragging && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (dragging && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
if (event.getTypeInt() == Event.ONTOUCHMOVE) {
/*
* prevent using this event in e.g. scrolling
@@ -3231,7 +3234,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
moved = true;
}
- final int clientX = Util.getTouchOrMouseClientX(event);
+ final int clientX = WidgetUtil
+ .getTouchOrMouseClientX(event);
final int x = clientX + tHead.hTableWrapper.getScrollLeft();
int slotX = headerX;
closestSlot = colIndex;
@@ -3269,7 +3273,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
private void onResizeEvent(Event event) {
switch (DOM.eventGetType(event)) {
case Event.ONMOUSEDOWN:
- if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
return;
}
isResizing = true;
@@ -3280,7 +3284,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
DOM.eventPreventDefault(event);
break;
case Event.ONMOUSEUP:
- if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
return;
}
isResizing = false;
@@ -3297,7 +3301,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
fireColumnResizeEvent(cid, originalWidth, getColWidth(cid));
break;
case Event.ONMOUSEMOVE:
- if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
return;
}
if (isResizing) {
@@ -4739,7 +4743,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
*/
public int getRequiredHeight() {
return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight()
- + Util.getRequiredHeight(table);
+ + WidgetUtil.getRequiredHeight(table);
}
private void constructDOM() {
@@ -5953,8 +5957,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
if (!BrowserInfo.get().isAndroid()) {
event.preventDefault();
event.stopPropagation();
- Util.simulateClickFromTouchEvent(touchStart,
- this);
+ WidgetUtil.simulateClickFromTouchEvent(
+ touchStart, this);
}
touchStart = null;
}
@@ -6013,7 +6017,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
*/
if (mouseUpPreviewMatched
&& lastMouseDownTarget != null
- && lastMouseDownTarget == getElementTdOrTr(Util
+ && lastMouseDownTarget == getElementTdOrTr(WidgetUtil
.getElementUnderMouse(event))) {
// "Click" with left, right or middle button
@@ -6140,7 +6144,8 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
* Touch has not been handled as neither context or
* drag start, handle it as a click.
*/
- Util.simulateClickFromTouchEvent(touchStart, this);
+ WidgetUtil.simulateClickFromTouchEvent(touchStart,
+ this);
touchStart = null;
}
touchContextProvider.cancel();
@@ -6242,7 +6247,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
* the corresponding mouseup event if it's on a
* different part of the page.
*/
- lastMouseDownTarget = getElementTdOrTr(Util
+ lastMouseDownTarget = getElementTdOrTr(WidgetUtil
.getElementUnderMouse(event));
mouseUpPreviewMatched = false;
mouseUpEventPreviewRegistration = Event
@@ -6387,7 +6392,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
private Element getElementTdOrTr(Element element) {
- Widget widget = Util.findWidget(element, null);
+ Widget widget = WidgetUtil.findWidget(element, null);
if (widget != this) {
/*
@@ -6414,9 +6419,9 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
public void showContextMenu(Event event) {
if (enabled && actionKeys != null) {
// Show context menu if there are registered action handlers
- int left = Util.getTouchOrMouseClientX(event)
+ int left = WidgetUtil.getTouchOrMouseClientX(event)
+ Window.getScrollLeft();
- int top = Util.getTouchOrMouseClientY(event)
+ int top = WidgetUtil.getTouchOrMouseClientY(event)
+ Window.getScrollTop();
showContextMenu(left, top);
}
@@ -6695,8 +6700,9 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
.getVisibleCellCount(); ix++) {
spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
}
- Util.setWidthExcludingPaddingAndBorder((Element) getElement()
- .getChild(cellIx), spanWidth, 13, false);
+ WidgetUtil.setWidthExcludingPaddingAndBorder(
+ (Element) getElement().getChild(cellIx), spanWidth, 13,
+ false);
}
}
@@ -6894,7 +6900,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
int totalExtraWidth = scrollBody.getCellExtraWidth()
* visibleCellCount;
if (willHaveScrollbars()) {
- totalExtraWidth += Util.getNativeScrollbarSize();
+ totalExtraWidth += WidgetUtil.getNativeScrollbarSize();
// if there will be vertical scrollbar, let's enable it
scrollBodyPanel.getElement().getStyle().clearOverflowY();
} else {
@@ -7045,7 +7051,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
int bodyHeight = scrollBody.getRequiredHeight();
boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth);
if (needsSpaceForHorizontalScrollbar) {
- bodyHeight += Util.getNativeScrollbarSize();
+ bodyHeight += WidgetUtil.getNativeScrollbarSize();
}
int heightBefore = getOffsetHeight();
scrollBodyPanel.setHeight(bodyHeight + "px");
@@ -7090,7 +7096,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
*/
private int getBorderWidth() {
if (borderWidth < 0) {
- borderWidth = Util.measureHorizontalPaddingAndBorder(
+ borderWidth = WidgetUtil.measureHorizontalPaddingAndBorder(
scrollBodyPanel.getElement(), 2);
if (borderWidth < 0) {
borderWidth = 0;
@@ -7433,7 +7439,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
Class<? extends Widget> clazz = getRowClass();
VScrollTableRow row = null;
if (clazz != null) {
- row = Util.findWidget(elementOver, clazz);
+ row = WidgetUtil.findWidget(elementOver, clazz);
}
if (row != null) {
dropDetails.overkey = row.rowKey;
@@ -7625,7 +7631,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
* FIXME The next line doesn't always do what expected, because if the
* row is not in the DOM it won't scroll to it.
*/
- Util.scrollIntoViewVertically(row.getElement());
+ WidgetUtil.scrollIntoViewVertically(row.getElement());
}
/**
@@ -7926,7 +7932,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
* ...and sometimes it sends blur events even though the focus
* handler is still active. (#10464)
*/
- Element focusedElement = Util.getIEFocusedElement();
+ Element focusedElement = WidgetUtil.getFocusedElement();
if (Util.getConnectorForElement(client, getParent(), focusedElement) == this
&& focusedElement != null
&& focusedElement != scrollBodyPanel.getFocusElement()) {
@@ -8235,7 +8241,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets,
@Override
public String getSubPartName(com.google.gwt.user.client.Element subElement) {
- Widget widget = Util.findWidget(subElement, null);
+ Widget widget = WidgetUtil.findWidget(subElement, null);
if (widget instanceof HeaderCell) {
return SUBPART_HEADER + "[" + tHead.visibleCells.indexOf(widget)
+ "]";
diff --git a/client/src/com/vaadin/client/ui/VSlider.java b/client/src/com/vaadin/client/ui/VSlider.java
index 27c8522f37..f5769ddf74 100644
--- a/client/src/com/vaadin/client/ui/VSlider.java
+++ b/client/src/com/vaadin/client/ui/VSlider.java
@@ -34,7 +34,7 @@ import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasValue;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.slider.SliderOrientation;
public class VSlider extends SimpleFocusablePanel implements Field,
@@ -299,7 +299,7 @@ public class VSlider extends SimpleFocusablePanel implements Field,
} else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
feedbackPopup.show();
}
- if (Util.isTouchEvent(event)) {
+ if (WidgetUtil.isTouchEvent(event)) {
event.preventDefault(); // avoid simulated events
event.stopPropagation();
}
@@ -423,9 +423,9 @@ public class VSlider extends SimpleFocusablePanel implements Field,
*/
protected int getEventPosition(Event event) {
if (isVertical()) {
- return Util.getTouchOrMouseClientY(event);
+ return WidgetUtil.getTouchOrMouseClientY(event);
} else {
- return Util.getTouchOrMouseClientX(event);
+ return WidgetUtil.getTouchOrMouseClientX(event);
}
}
diff --git a/client/src/com/vaadin/client/ui/VTabsheet.java b/client/src/com/vaadin/client/ui/VTabsheet.java
index 96af09bb32..c8984ece51 100644
--- a/client/src/com/vaadin/client/ui/VTabsheet.java
+++ b/client/src/com/vaadin/client/ui/VTabsheet.java
@@ -63,7 +63,7 @@ import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.Focusable;
import com.vaadin.client.TooltipInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VCaption;
import com.vaadin.client.VTooltip;
import com.vaadin.client.ui.aria.AriaHelper;
@@ -415,7 +415,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, SubPartAware
public int getRequiredWidth() {
int width = super.getRequiredWidth();
if (closeButton != null) {
- width += Util.getRequiredWidth(closeButton);
+ width += WidgetUtil.getRequiredWidth(closeButton);
}
return width;
}
@@ -1330,7 +1330,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, SubPartAware
/** For internal use only. May be removed or replaced in the future. */
public int getContentAreaBorderWidth() {
- return Util.measureHorizontalBorder(contentNode);
+ return WidgetUtil.measureHorizontalBorder(contentNode);
}
@Override
diff --git a/client/src/com/vaadin/client/ui/VTextArea.java b/client/src/com/vaadin/client/ui/VTextArea.java
index bb48b29e61..50930f2fee 100644
--- a/client/src/com/vaadin/client/ui/VTextArea.java
+++ b/client/src/com/vaadin/client/ui/VTextArea.java
@@ -32,7 +32,7 @@ import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.vaadin.client.BrowserInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.dd.DragImageModifier;
/**
@@ -310,7 +310,7 @@ public class VTextArea extends VTextField implements DragImageModifier {
// and reattach the whole TextArea.
// Webkit fails to properly reflow the text when enabling wrapping,
// same workaround
- Util.detachAttach(getElement());
+ WidgetUtil.detachAttach(getElement());
}
this.wordwrap = wordwrap;
}
diff --git a/client/src/com/vaadin/client/ui/VTextField.java b/client/src/com/vaadin/client/ui/VTextField.java
index b402ced218..1554bd1a22 100644
--- a/client/src/com/vaadin/client/ui/VTextField.java
+++ b/client/src/com/vaadin/client/ui/VTextField.java
@@ -34,7 +34,7 @@ import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.TextBoxBase;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.EventId;
import com.vaadin.shared.ui.textfield.TextFieldConstants;
@@ -422,7 +422,7 @@ public class VTextField extends TextBoxBase implements Field, ChangeHandler,
* @return true iff the value was updated
*/
protected boolean updateCursorPosition() {
- if (Util.isAttachedAndDisplayed(this)) {
+ if (WidgetUtil.isAttachedAndDisplayed(this)) {
int cursorPos = getCursorPos();
if (lastCursorPos != cursorPos) {
client.updateVariable(paintableId,
diff --git a/client/src/com/vaadin/client/ui/VTree.java b/client/src/com/vaadin/client/ui/VTree.java
index 82ffaced1f..6539eb49a9 100644
--- a/client/src/com/vaadin/client/ui/VTree.java
+++ b/client/src/com/vaadin/client/ui/VTree.java
@@ -60,6 +60,7 @@ import com.vaadin.client.ConnectorMap;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.aria.AriaHelper;
import com.vaadin.client.ui.aria.HandlesAriaCaption;
import com.vaadin.client.ui.dd.DDUtil;
@@ -346,7 +347,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
}
private String findCurrentMouseOverKey(Element elementOver) {
- TreeNode treeNode = Util.findWidget(elementOver, TreeNode.class);
+ TreeNode treeNode = WidgetUtil.findWidget(elementOver, TreeNode.class);
return treeNode == null ? null : treeNode.key;
}
@@ -1132,7 +1133,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
* Scrolls the caption into view
*/
public void scrollIntoView() {
- Util.scrollIntoViewVertically(nodeCaptionDiv);
+ WidgetUtil.scrollIntoViewVertically(nodeCaptionDiv);
}
public void setIcon(String iconUrl, String altText) {
@@ -2143,7 +2144,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler,
return "fe";
}
- TreeNode treeNode = Util.findWidget(subElement, TreeNode.class);
+ TreeNode treeNode = WidgetUtil.findWidget(subElement, TreeNode.class);
if (treeNode == null) {
// Did not click on a node, let somebody else take care of the
// locator string
diff --git a/client/src/com/vaadin/client/ui/VTreeTable.java b/client/src/com/vaadin/client/ui/VTreeTable.java
index 9e5940a2f2..0ba84af4bb 100644
--- a/client/src/com/vaadin/client/ui/VTreeTable.java
+++ b/client/src/com/vaadin/client/ui/VTreeTable.java
@@ -37,7 +37,7 @@ import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComputedStyle;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VTreeTable.VTreeTableScrollBody.VTreeTableRow;
public class VTreeTable extends VScrollTable {
@@ -418,8 +418,9 @@ public class VTreeTable extends VScrollTable {
.getVisibleCellCount(); ix++) {
spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
}
- Util.setWidthExcludingPaddingAndBorder((Element) getElement()
- .getChild(cellIx), spanWidth, 13, false);
+ WidgetUtil.setWidthExcludingPaddingAndBorder(
+ (Element) getElement().getChild(cellIx), spanWidth, 13,
+ false);
}
}
diff --git a/client/src/com/vaadin/client/ui/VTwinColSelect.java b/client/src/com/vaadin/client/ui/VTwinColSelect.java
index 3987460989..853bd8d456 100644
--- a/client/src/com/vaadin/client/ui/VTwinColSelect.java
+++ b/client/src/com/vaadin/client/ui/VTwinColSelect.java
@@ -37,9 +37,9 @@ 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.client.ApplicationConnection;
+import com.vaadin.client.StyleConstants;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.twincolselect.TwinColSelectConstants;
public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler,
@@ -352,7 +352,7 @@ public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler,
/** For internal use only. May be removed or replaced in the future. */
public void setInternalHeights() {
- int captionHeight = Util.getRequiredHeight(captionWrapper);
+ int captionHeight = WidgetUtil.getRequiredHeight(captionWrapper);
int totalHeight = getOffsetHeight();
String selectHeight = (totalHeight - captionHeight) + "px";
@@ -394,10 +394,10 @@ public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler,
/** For internal use only. May be removed or replaced in the future. */
public void setInternalWidths() {
getElement().getStyle().setPosition(Position.RELATIVE);
- int bordersAndPaddings = Util.measureHorizontalPaddingAndBorder(
+ int bordersAndPaddings = WidgetUtil.measureHorizontalPaddingAndBorder(
buttons.getElement(), 0);
- int buttonWidth = Util.getRequiredWidth(buttons);
+ int buttonWidth = WidgetUtil.getRequiredWidth(buttons);
int totalWidth = getOffsetWidth();
int spaceForSelect = (totalWidth - buttonWidth - bordersAndPaddings) / 2;
@@ -429,8 +429,8 @@ public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler,
selections.setEnabled(enabled);
add.setEnabled(enabled);
remove.setEnabled(enabled);
- add.setStyleName(ApplicationConnection.DISABLED_CLASSNAME, !enabled);
- remove.setStyleName(ApplicationConnection.DISABLED_CLASSNAME, !enabled);
+ add.setStyleName(StyleConstants.DISABLED, !enabled);
+ remove.setStyleName(StyleConstants.DISABLED, !enabled);
}
@Override
@@ -609,14 +609,14 @@ public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler,
if (options.getElement() == subElement) {
return SUBPART_OPTION_SELECT;
} else {
- int idx = Util.getChildElementIndex(subElement);
+ int idx = WidgetUtil.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);
+ int idx = WidgetUtil.getChildElementIndex(subElement);
return SUBPART_SELECTION_SELECT_ITEM + idx;
}
} else if (add.getElement().isOrHasChild(subElement)) {
diff --git a/client/src/com/vaadin/client/ui/VUI.java b/client/src/com/vaadin/client/ui/VUI.java
index eae4f6319d..0c1b83ab0f 100644
--- a/client/src/com/vaadin/client/ui/VUI.java
+++ b/client/src/com/vaadin/client/ui/VUI.java
@@ -44,7 +44,7 @@ import com.vaadin.client.ConnectorMap;
import com.vaadin.client.Focusable;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.Profiler;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler;
@@ -501,7 +501,7 @@ public class VUI extends SimplePanel implements ResizeHandler,
* @param focusedElement
*/
public void storeFocus() {
- storedFocus = Util.getFocusedElement();
+ storedFocus = WidgetUtil.getFocusedElement();
}
/**
diff --git a/client/src/com/vaadin/client/ui/VUpload.java b/client/src/com/vaadin/client/ui/VUpload.java
index 42fb08fb3c..dff45a6951 100644
--- a/client/src/com/vaadin/client/ui/VUpload.java
+++ b/client/src/com/vaadin/client/ui/VUpload.java
@@ -36,6 +36,7 @@ import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.StyleConstants;
import com.vaadin.client.VConsole;
import com.vaadin.client.ui.upload.UploadIFrameOnloadStrategy;
@@ -211,8 +212,7 @@ public class VUpload extends SimplePanel {
private void setEnabledForSubmitButton(boolean enabled) {
submitButton.setEnabled(enabled);
- submitButton.setStyleName(ApplicationConnection.DISABLED_CLASSNAME,
- !enabled);
+ submitButton.setStyleName(StyleConstants.DISABLED, !enabled);
}
/**
diff --git a/client/src/com/vaadin/client/ui/VWindow.java b/client/src/com/vaadin/client/ui/VWindow.java
index 786ecf1267..6977cf9e7f 100644
--- a/client/src/com/vaadin/client/ui/VWindow.java
+++ b/client/src/com/vaadin/client/ui/VWindow.java
@@ -16,7 +16,7 @@
package com.vaadin.client.ui;
-import static com.vaadin.client.Util.isFocusedElementEditable;
+import static com.vaadin.client.WidgetUtil.isFocusedElementEditable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -62,7 +62,7 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.Focusable;
import com.vaadin.client.LayoutManager;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.debug.internal.VDebugWindow;
import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
import com.vaadin.client.ui.aria.AriaHelper;
@@ -581,7 +581,8 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
* ticket #11994 which was changing the size to 110% was replaced
* with this due to ticket #12943
*/
- Util.runWebkitOverflowAutoFix(contents.getFirstChildElement());
+ WidgetUtil
+ .runWebkitOverflowAutoFix(contents.getFirstChildElement());
}
}
@@ -884,7 +885,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
if (asHtml) {
html = c == null ? "" : c;
} else {
- html = Util.escapeHTML(c);
+ html = WidgetUtil.escapeHTML(c);
}
// Provide information to assistive device users that a sub window was
// opened
@@ -1043,7 +1044,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
}
private void onResizeEvent(Event event) {
- if (resizable && Util.isTouchEventOrLeftMouseButton(event)) {
+ if (resizable && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
switch (event.getTypeInt()) {
case Event.ONMOUSEDOWN:
case Event.ONTOUCHSTART:
@@ -1055,8 +1056,8 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
resizeBox.getStyle().setVisibility(Visibility.HIDDEN);
}
resizing = true;
- startX = Util.getTouchOrMouseClientX(event);
- startY = Util.getTouchOrMouseClientY(event);
+ startX = WidgetUtil.getTouchOrMouseClientX(event);
+ startY = WidgetUtil.getTouchOrMouseClientY(event);
origW = getElement().getOffsetWidth();
origH = getElement().getOffsetHeight();
DOM.setCapture(getElement());
@@ -1122,8 +1123,8 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
return;
}
- int w = Util.getTouchOrMouseClientX(event) - startX + origW;
- int h = Util.getTouchOrMouseClientY(event) - startY + origH;
+ int w = WidgetUtil.getTouchOrMouseClientX(event) - startX + origW;
+ int h = WidgetUtil.getTouchOrMouseClientY(event) - startY + origH;
w = Math.max(w, getMinWidth());
h = Math.max(h, getMinHeight());
@@ -1186,7 +1187,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
}
private void onDragEvent(Event event) {
- if (!Util.isTouchEventOrLeftMouseButton(event)) {
+ if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
return;
}
@@ -1221,9 +1222,9 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
centered = false;
if (cursorInsideBrowserContentArea(event)) {
// Only drag while cursor is inside the browser client area
- final int x = Util.getTouchOrMouseClientX(event) - startX
+ final int x = WidgetUtil.getTouchOrMouseClientX(event) - startX
+ origX;
- final int y = Util.getTouchOrMouseClientY(event) - startY
+ final int y = WidgetUtil.getTouchOrMouseClientY(event) - startY
+ origY;
setPopupPosition(x, y);
}
@@ -1235,8 +1236,8 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
if (draggable) {
showDraggingCurtain();
dragging = true;
- startX = Util.getTouchOrMouseClientX(event);
- startY = Util.getTouchOrMouseClientY(event);
+ startX = WidgetUtil.getTouchOrMouseClientX(event);
+ startY = WidgetUtil.getTouchOrMouseClientY(event);
origX = DOM.getAbsoluteLeft(getElement());
origY = DOM.getAbsoluteTop(getElement());
DOM.setCapture(getElement());
@@ -1284,7 +1285,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
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);
+ Widget w = WidgetUtil.findWidget(target, null);
while (w != null) {
if (w instanceof VDebugWindow) {
return true; // allow debug-window clicks
diff --git a/client/src/com/vaadin/client/ui/accordion/AccordionConnector.java b/client/src/com/vaadin/client/ui/accordion/AccordionConnector.java
index 72aa2dbdfd..949e19071c 100644
--- a/client/src/com/vaadin/client/ui/accordion/AccordionConnector.java
+++ b/client/src/com/vaadin/client/ui/accordion/AccordionConnector.java
@@ -17,7 +17,7 @@ package com.vaadin.client.ui.accordion;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.SimpleManagedLayout;
import com.vaadin.client.ui.VAccordion;
@@ -106,7 +106,8 @@ public class AccordionConnector extends TabsheetBaseConnector implements
usedPixels += item.getCaptionHeight();
} else {
// This includes the captionNode borders
- usedPixels += Util.getRequiredHeight(item.getElement());
+ usedPixels += WidgetUtil.getRequiredHeight(item
+ .getElement());
}
}
int rootElementInnerHeight = getLayoutManager().getInnerHeight(
diff --git a/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java b/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java
index 8c92ef1233..e9bbf2015c 100644
--- a/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java
+++ b/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java
@@ -35,7 +35,7 @@ import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.Paintable;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
import com.vaadin.client.communication.RpcProxy;
import com.vaadin.client.communication.StateChangeEvent;
@@ -422,7 +422,7 @@ public class CalendarConnector extends AbstractComponentConnector implements
@Override
public TooltipInfo getTooltipInfo(com.google.gwt.dom.client.Element element) {
TooltipInfo tooltipInfo = null;
- Widget w = Util.findWidget(element, null);
+ Widget w = WidgetUtil.findWidget(element, null);
if (w instanceof HasTooltipKey) {
tooltipInfo = GWT.create(TooltipInfo.class);
String title = tooltips.get(((HasTooltipKey) w).getTooltipKey());
diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java
index 448083ba26..39d516b694 100644
--- a/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java
+++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCell.java
@@ -43,7 +43,7 @@ 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.Widget;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
public class DateCell extends FocusableComplexPanel implements
MouseDownHandler, MouseMoveHandler, MouseUpHandler, KeyDownHandler,
@@ -201,7 +201,7 @@ public class DateCell extends FocusableComplexPanel implements
addStyleDependentName("Hsized");
width = getOffsetWidth()
- - Util.measureHorizontalBorder(getElement());
+ - WidgetUtil.measureHorizontalBorder(getElement());
// Update moveWidth for any DateCellDayEvent child
updateEventCellsWidth();
recalculateEventWidths();
@@ -338,7 +338,7 @@ public class DateCell extends FocusableComplexPanel implements
}
public int getSlotBorder() {
- return Util.measureVerticalBorder(slotElements[0]);
+ return WidgetUtil.measureVerticalBorder(slotElements[0]);
}
private void drawDayEvents(List<DateCellGroup> groups) {
diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java
index 82af89c794..26f5951987 100644
--- a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java
+++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java
@@ -23,7 +23,7 @@ import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Widget;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VCalendar;
/**
@@ -48,7 +48,7 @@ public class DateCellContainer extends FlowPanel implements MouseDownHandler,
public static int measureBorderWidth(DateCellContainer dc) {
if (borderWidth == -1) {
- borderWidth = Util.measureHorizontalBorder(dc.getElement());
+ borderWidth = WidgetUtil.measureHorizontalBorder(dc.getElement());
}
return borderWidth;
}
diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java
index 8b08e9bc7a..1a54fe0454 100644
--- a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java
+++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java
@@ -41,7 +41,7 @@ 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.HorizontalPanel;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.calendar.DateConstants;
/**
@@ -190,7 +190,7 @@ public class DateCellDayEvent extends FocusableHTML implements
if (dateCell.weekgrid.getCalendar().isEventCaptionAsHtml()) {
htmlOrText = calendarEvent.getCaption();
} else {
- htmlOrText = Util.escapeHTML(calendarEvent.getCaption());
+ htmlOrText = WidgetUtil.escapeHTML(calendarEvent.getCaption());
}
if (bigMode) {
diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java
index 545ddadc52..aecaff1931 100644
--- a/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java
+++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeekGrid.java
@@ -31,7 +31,7 @@ import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.DateTimeService;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VCalendar;
import com.vaadin.shared.ui.calendar.DateConstants;
@@ -160,7 +160,7 @@ public class WeekGrid extends SimplePanel {
// Otherwise the scroll wrapper is somehow too narrow = horizontal
// scroll
wrapper.setWidth(content.getOffsetWidth()
- + Util.getNativeScrollbarSize() + "px");
+ + WidgetUtil.getNativeScrollbarSize() + "px");
this.width = content.getOffsetWidth() - timebar.getOffsetWidth();
@@ -169,7 +169,7 @@ public class WeekGrid extends SimplePanel {
- timebar.getOffsetWidth();
if (isVerticalScrollable() && width != -1) {
- this.width = this.width - Util.getNativeScrollbarSize();
+ this.width = this.width - WidgetUtil.getNativeScrollbarSize();
}
updateCellWidths();
}
diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java
index 9cab421200..39e08e9d70 100644
--- a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java
+++ b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java
@@ -17,7 +17,7 @@ package com.vaadin.client.ui.calendar.schedule.dd;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.calendar.CalendarConnector;
import com.vaadin.client.ui.calendar.schedule.SimpleDayCell;
import com.vaadin.client.ui.dd.VAcceptCallback;
@@ -51,7 +51,7 @@ public class CalendarMonthDropHandler extends CalendarDropHandler {
protected void dragAccepted(VDragEvent drag) {
deEmphasis();
currentTargetElement = drag.getElementOver();
- currentTargetDay = Util.findWidget(currentTargetElement,
+ currentTargetDay = WidgetUtil.findWidget(currentTargetElement,
SimpleDayCell.class);
emphasis();
}
diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java
index 853e4b724e..e0edf21e89 100644
--- a/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java
+++ b/client/src/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java
@@ -17,7 +17,7 @@ package com.vaadin.client.ui.calendar.schedule.dd;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.DOM;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.calendar.CalendarConnector;
import com.vaadin.client.ui.calendar.schedule.DateCell;
import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent;
@@ -52,8 +52,8 @@ public class CalendarWeekDropHandler extends CalendarDropHandler {
protected void dragAccepted(VDragEvent drag) {
deEmphasis();
currentTargetElement = drag.getElementOver();
- currentTargetDay = Util
- .findWidget(currentTargetElement, DateCell.class);
+ currentTargetDay = WidgetUtil.findWidget(currentTargetElement,
+ DateCell.class);
emphasis();
}
@@ -121,7 +121,7 @@ public class CalendarWeekDropHandler extends CalendarDropHandler {
return DOM.isOrHasChild(weekGridElement, elementOver)
&& !DOM.isOrHasChild(timeBarElement, elementOver)
&& todayBarElement != elementOver
- && (Util.findWidget(elementOver, DateCellDayEvent.class) == null);
+ && (WidgetUtil.findWidget(elementOver, DateCellDayEvent.class) == null);
}
/*
diff --git a/client/src/com/vaadin/client/ui/dd/DDUtil.java b/client/src/com/vaadin/client/ui/dd/DDUtil.java
index 77de1f9b1a..fdccd61767 100644
--- a/client/src/com/vaadin/client/ui/dd/DDUtil.java
+++ b/client/src/com/vaadin/client/ui/dd/DDUtil.java
@@ -18,7 +18,7 @@ package com.vaadin.client.ui.dd;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.user.client.Window;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.dd.HorizontalDropLocation;
import com.vaadin.shared.ui.dd.VerticalDropLocation;
@@ -33,7 +33,7 @@ public class DDUtil {
public static VerticalDropLocation getVerticalDropLocation(Element element,
int offsetHeight, NativeEvent event, double topBottomRatio) {
- int clientY = Util.getTouchOrMouseClientY(event);
+ int clientY = WidgetUtil.getTouchOrMouseClientY(event);
return getVerticalDropLocation(element, offsetHeight, clientY,
topBottomRatio);
}
@@ -59,7 +59,7 @@ public class DDUtil {
public static HorizontalDropLocation getHorizontalDropLocation(
Element element, NativeEvent event, double leftRightRatio) {
- int clientX = Util.getTouchOrMouseClientX(event);
+ int clientX = WidgetUtil.getTouchOrMouseClientX(event);
// Event coordinates are relative to the viewport, element absolute
// position is relative to the document. Make element position relative
diff --git a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java
index 4ee19328d6..844f4c1b9c 100644
--- a/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java
+++ b/client/src/com/vaadin/client/ui/dd/VDragAndDropManager.java
@@ -38,7 +38,7 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.Profiler;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
import com.vaadin.client.ValueMap;
import com.vaadin.client.ui.VOverlay;
@@ -92,16 +92,16 @@ public class VDragAndDropManager {
targetElement = targetNode.getParentElement();
}
- if (Util.isTouchEvent(nativeEvent) || dragElement != null) {
+ if (WidgetUtil.isTouchEvent(nativeEvent) || dragElement != null) {
// 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);
+ int x = WidgetUtil.getTouchOrMouseClientX(nativeEvent);
+ int y = WidgetUtil.getTouchOrMouseClientY(nativeEvent);
// Util.browserDebugger();
- targetElement = Util.getElementFromPoint(x, y);
+ targetElement = WidgetUtil.getElementFromPoint(x, y);
if (targetElement == null) {
// ApplicationConnection.getConsole().log(
// "Event on dragImage, ignored");
@@ -361,10 +361,10 @@ public class VDragAndDropManager {
deferredStartRegistration = Event
.addNativePreviewHandler(new NativePreviewHandler() {
- private int startX = Util
+ private int startX = WidgetUtil
.getTouchOrMouseClientX(currentDrag
.getCurrentGwtEvent());
- private int startY = Util
+ private int startY = WidgetUtil
.getTouchOrMouseClientY(currentDrag
.getCurrentGwtEvent());
@@ -419,10 +419,10 @@ public class VDragAndDropManager {
}
case Event.ONMOUSEMOVE:
case Event.ONTOUCHMOVE:
- int currentX = Util
+ int currentX = WidgetUtil
.getTouchOrMouseClientX(event
.getNativeEvent());
- int currentY = Util
+ int currentY = WidgetUtil
.getTouchOrMouseClientY(event
.getNativeEvent());
if (Math.abs(startX - currentX) > 3
@@ -462,9 +462,9 @@ public class VDragAndDropManager {
private void updateDragImagePosition() {
if (currentDrag.getCurrentGwtEvent() != null && dragElement != null) {
Style style = dragElement.getStyle();
- int clientY = Util.getTouchOrMouseClientY(currentDrag
+ int clientY = WidgetUtil.getTouchOrMouseClientY(currentDrag
.getCurrentGwtEvent());
- int clientX = Util.getTouchOrMouseClientX(currentDrag
+ int clientX = WidgetUtil.getTouchOrMouseClientX(currentDrag
.getCurrentGwtEvent());
style.setTop(clientY, Unit.PX);
style.setLeft(clientX, Unit.PX);
@@ -480,7 +480,7 @@ public class VDragAndDropManager {
*/
private VDropHandler findDragTarget(Element element) {
try {
- Widget w = Util.findWidget(element, null);
+ Widget w = WidgetUtil.findWidget(element, null);
if (w == null) {
return null;
}
diff --git a/client/src/com/vaadin/client/ui/dd/VDragEvent.java b/client/src/com/vaadin/client/ui/dd/VDragEvent.java
index 45f89bdb87..c889dbf34e 100644
--- a/client/src/com/vaadin/client/ui/dd/VDragEvent.java
+++ b/client/src/com/vaadin/client/ui/dd/VDragEvent.java
@@ -30,7 +30,7 @@ import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.vaadin.client.BrowserInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
/**
* DragEvent used by Vaadin client side engine. Supports components, items,
@@ -262,8 +262,8 @@ public class VDragEvent {
if (alignImageToEvent) {
int absoluteTop = element.getAbsoluteTop();
int absoluteLeft = element.getAbsoluteLeft();
- int clientX = Util.getTouchOrMouseClientX(startEvent);
- int clientY = Util.getTouchOrMouseClientY(startEvent);
+ int clientX = WidgetUtil.getTouchOrMouseClientX(startEvent);
+ int clientY = WidgetUtil.getTouchOrMouseClientY(startEvent);
int offsetX = absoluteLeft - clientX;
int offsetY = absoluteTop - clientY;
setDragImage(cloneNode, offsetX, offsetY);
diff --git a/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java b/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java
index 494a1a87ff..9517619cf3 100644
--- a/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java
+++ b/client/src/com/vaadin/client/ui/formlayout/FormLayoutConnector.java
@@ -20,7 +20,7 @@ import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.TooltipInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractFieldConnector;
import com.vaadin.client.ui.AbstractLayoutConnector;
@@ -114,14 +114,16 @@ public class FormLayoutConnector extends AbstractLayoutConnector {
TooltipInfo info = null;
if (element != getWidget().getElement()) {
- Object node = Util.findWidget(element, VFormLayout.Caption.class);
+ Object node = WidgetUtil.findWidget(element,
+ VFormLayout.Caption.class);
if (node != null) {
VFormLayout.Caption caption = (VFormLayout.Caption) node;
info = caption.getOwner().getTooltipInfo(element);
} else {
- node = Util.findWidget(element, VFormLayout.ErrorFlag.class);
+ node = WidgetUtil.findWidget(element,
+ VFormLayout.ErrorFlag.class);
if (node != null) {
VFormLayout.ErrorFlag flag = (VFormLayout.ErrorFlag) node;
diff --git a/client/src/com/vaadin/client/ui/label/LabelConnector.java b/client/src/com/vaadin/client/ui/label/LabelConnector.java
index 07defcc64d..fc94f27cf0 100644
--- a/client/src/com/vaadin/client/ui/label/LabelConnector.java
+++ b/client/src/com/vaadin/client/ui/label/LabelConnector.java
@@ -18,7 +18,7 @@ package com.vaadin.client.ui.label;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.PreElement;
import com.vaadin.client.Profiler;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.VLabel;
@@ -69,7 +69,7 @@ public class LabelConnector extends AbstractComponentConnector {
if (sinkOnloads) {
Profiler.enter("LabelConnector.onStateChanged sinkOnloads");
- Util.sinkOnloadForImages(getWidget().getElement());
+ WidgetUtil.sinkOnloadForImages(getWidget().getElement());
Profiler.leave("LabelConnector.onStateChanged sinkOnloads");
}
}
diff --git a/client/src/com/vaadin/client/ui/layout/LayoutDependencyTree.java b/client/src/com/vaadin/client/ui/layout/LayoutDependencyTree.java
index ae866e3354..da3aed4bbc 100644
--- a/client/src/com/vaadin/client/ui/layout/LayoutDependencyTree.java
+++ b/client/src/com/vaadin/client/ui/layout/LayoutDependencyTree.java
@@ -586,7 +586,7 @@ public class LayoutDependencyTree {
}
private static String getCompactConnectorString(ServerConnector connector) {
- return Util.getSimpleName(connector) + " ("
+ return connector.getClass().getSimpleName() + " ("
+ connector.getConnectorId() + ")";
}
diff --git a/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java b/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java
index 20cabf9a36..03eeb85165 100644
--- a/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java
+++ b/client/src/com/vaadin/client/ui/menubar/MenuBarConnector.java
@@ -25,7 +25,7 @@ import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.Paintable;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.ImageIcon;
import com.vaadin.client.ui.SimpleManagedLayout;
@@ -78,7 +78,7 @@ public class MenuBarConnector extends AbstractComponentConnector implements
if (moreItemUIDL.hasAttribute("icon")) {
itemHTML.append("<img src=\""
- + Util.escapeAttribute(client
+ + WidgetUtil.escapeAttribute(client
.translateVaadinUri(moreItemUIDL
.getStringAttribute("icon")))
+ "\" class=\"" + ImageIcon.CLASSNAME
diff --git a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java
index c2157650a5..8fa885c2b9 100644
--- a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java
+++ b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java
@@ -30,6 +30,7 @@ import com.vaadin.client.Profiler;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler;
import com.vaadin.client.ui.AbstractFieldConnector;
@@ -387,7 +388,7 @@ public abstract class AbstractOrderedLayoutConnector extends
@Override
public TooltipInfo getTooltipInfo(com.google.gwt.dom.client.Element element) {
if (element != getWidget().getElement()) {
- Slot slot = Util.findWidget(element, Slot.class);
+ Slot slot = WidgetUtil.findWidget(element, Slot.class);
if (slot != null && slot.getCaptionElement() != null
&& slot.getParent() == getWidget()
&& slot.getCaptionElement().isOrHasChild(element)) {
diff --git a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java
index 4b60f11ab4..b97cf73989 100644
--- a/client/src/com/vaadin/client/ui/orderedlayout/Slot.java
+++ b/client/src/com/vaadin/client/ui/orderedlayout/Slot.java
@@ -30,7 +30,7 @@ import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.StyleConstants;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.FontIcon;
import com.vaadin.client.ui.Icon;
import com.vaadin.client.ui.ImageIcon;
@@ -74,7 +74,7 @@ public final class Slot extends SimplePanel {
public void onElementResize(ElementResizeEvent e) {
Element caption = getCaptionElement();
if (caption != null) {
- Util.forceIE8Redraw(caption);
+ WidgetUtil.forceIE8Redraw(caption);
}
}
};
@@ -493,7 +493,7 @@ public final class Slot extends SimplePanel {
// Caption wrappers
Widget widget = getWidget();
- final Element focusedElement = Util.getFocusedElement();
+ final Element focusedElement = WidgetUtil.getFocusedElement();
// By default focus will not be lost
boolean focusLost = false;
if (captionText != null || icon != null || error != null || required) {
@@ -613,7 +613,7 @@ public final class Slot extends SimplePanel {
if (focusLost) {
// Find out what element is currently focused.
- Element currentFocus = Util.getFocusedElement();
+ Element currentFocus = WidgetUtil.getFocusedElement();
if (currentFocus != null
&& currentFocus.equals(Document.get().getBody())) {
// Focus has moved to BodyElement and should be moved back to
@@ -627,12 +627,12 @@ public final class Slot extends SimplePanel {
@Override
public void run() {
- if (Util.getFocusedElement() == null) {
+ if (WidgetUtil.getFocusedElement() == null) {
// This should never become an infinite loop and
// even if it does it will be stopped once something
// is done with the browser.
schedule(25);
- } else if (Util.getFocusedElement().equals(
+ } else if (WidgetUtil.getFocusedElement().equals(
Document.get().getBody())) {
// Focus found it's way to BodyElement. Now it can
// be restored
diff --git a/client/src/com/vaadin/client/ui/orderedlayout/VAbstractOrderedLayout.java b/client/src/com/vaadin/client/ui/orderedlayout/VAbstractOrderedLayout.java
index 4c74358753..2e6d4cf5c8 100644
--- a/client/src/com/vaadin/client/ui/orderedlayout/VAbstractOrderedLayout.java
+++ b/client/src/com/vaadin/client/ui/orderedlayout/VAbstractOrderedLayout.java
@@ -33,6 +33,7 @@ import com.vaadin.client.BrowserInfo;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.Profiler;
import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.MarginInfo;
/**
@@ -674,7 +675,7 @@ public class VAbstractOrderedLayout extends FlowPanel {
}
}
}
- Util.forceIE8Redraw(getElement());
+ WidgetUtil.forceIE8Redraw(getElement());
}
/**
diff --git a/client/src/com/vaadin/client/ui/table/TableConnector.java b/client/src/com/vaadin/client/ui/table/TableConnector.java
index 9f1f99bae3..0d34d2d4d9 100644
--- a/client/src/com/vaadin/client/ui/table/TableConnector.java
+++ b/client/src/com/vaadin/client/ui/table/TableConnector.java
@@ -31,7 +31,7 @@ import com.vaadin.client.Paintable;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.AbstractHasComponentsConnector;
import com.vaadin.client.ui.PostLayoutListener;
import com.vaadin.client.ui.VScrollTable;
@@ -342,7 +342,7 @@ public class TableConnector extends AbstractHasComponentsConnector implements
@Override
public void execute() {
// IE8 needs some hacks to measure sizes correctly
- Util.forceIE8Redraw(getWidget().getElement());
+ WidgetUtil.forceIE8Redraw(getWidget().getElement());
getLayoutManager().setNeedsMeasure(TableConnector.this);
ServerConnector parent = getParent();
@@ -394,7 +394,7 @@ public class TableConnector extends AbstractHasComponentsConnector implements
TooltipInfo info = null;
if (element != getWidget().getElement()) {
- Object node = Util.findWidget(element, VScrollTableRow.class);
+ Object node = WidgetUtil.findWidget(element, VScrollTableRow.class);
if (node != null) {
VScrollTableRow row = (VScrollTableRow) node;
diff --git a/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java b/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java
index d49581eaad..469fc6ba95 100644
--- a/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java
+++ b/client/src/com/vaadin/client/ui/tabsheet/TabsheetConnector.java
@@ -20,7 +20,7 @@ import com.google.gwt.dom.client.Style.Overflow;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.TooltipInfo;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.SimpleManagedLayout;
import com.vaadin.client.ui.VTabsheet;
@@ -139,7 +139,8 @@ public class TabsheetConnector extends TabsheetBaseConnector implements
// Find a tooltip for the tab, if the element is a tab
if (element != getWidget().getElement()) {
- Object node = Util.findWidget(element, VTabsheet.TabCaption.class);
+ Object node = WidgetUtil.findWidget(element,
+ VTabsheet.TabCaption.class);
if (node != null) {
VTabsheet.TabCaption caption = (VTabsheet.TabCaption) node;
diff --git a/client/src/com/vaadin/client/ui/textarea/TextAreaConnector.java b/client/src/com/vaadin/client/ui/textarea/TextAreaConnector.java
index e9dc3e1dd7..3bc0a86df4 100644
--- a/client/src/com/vaadin/client/ui/textarea/TextAreaConnector.java
+++ b/client/src/com/vaadin/client/ui/textarea/TextAreaConnector.java
@@ -19,7 +19,7 @@ package com.vaadin.client.ui.textarea;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
-import com.vaadin.client.Util.CssSize;
+import com.vaadin.client.WidgetUtil.CssSize;
import com.vaadin.client.ui.VTextArea;
import com.vaadin.client.ui.textfield.TextFieldConnector;
import com.vaadin.shared.ui.Connect;
diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java
index 55224b455f..fc3e6ca0fc 100644
--- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java
+++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java
@@ -27,7 +27,7 @@ import com.vaadin.client.BrowserInfo;
import com.vaadin.client.Paintable;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.VConsole;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractComponentConnector;
@@ -172,7 +172,7 @@ public class TreeConnector extends AbstractComponentConnector implements
}
// IE8 needs a hack to measure the tree again after update
- Util.forceIE8Redraw(getWidget().getElement());
+ WidgetUtil.forceIE8Redraw(getWidget().getElement());
getWidget().rendering = false;
@@ -333,7 +333,7 @@ public class TreeConnector extends AbstractComponentConnector implements
// Try to find a tooltip for a node
if (element != getWidget().getElement()) {
- Object node = Util.findWidget(element, TreeNode.class);
+ Object node = WidgetUtil.findWidget(element, TreeNode.class);
if (node != null) {
TreeNode tnode = (TreeNode) node;
diff --git a/client/src/com/vaadin/client/ui/treetable/TreeTableConnector.java b/client/src/com/vaadin/client/ui/treetable/TreeTableConnector.java
index 5a42484b28..4aab248e29 100644
--- a/client/src/com/vaadin/client/ui/treetable/TreeTableConnector.java
+++ b/client/src/com/vaadin/client/ui/treetable/TreeTableConnector.java
@@ -19,7 +19,7 @@ import com.google.gwt.dom.client.Element;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.TooltipInfo;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.FocusableScrollPanel;
import com.vaadin.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow;
import com.vaadin.client.ui.VTreeTable;
@@ -129,7 +129,7 @@ public class TreeTableConnector extends TableConnector {
TooltipInfo info = null;
if (element != getWidget().getElement()) {
- Object node = Util.findWidget(element, VTreeTableRow.class);
+ Object node = WidgetUtil.findWidget(element, VTreeTableRow.class);
if (node != null) {
VTreeTableRow row = (VTreeTableRow) node;
diff --git a/client/src/com/vaadin/client/ui/window/WindowConnector.java b/client/src/com/vaadin/client/ui/window/WindowConnector.java
index 33cabd563a..5214c33eec 100644
--- a/client/src/com/vaadin/client/ui/window/WindowConnector.java
+++ b/client/src/com/vaadin/client/ui/window/WindowConnector.java
@@ -38,7 +38,7 @@ import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.Paintable;
import com.vaadin.client.UIDL;
-import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
import com.vaadin.client.communication.RpcProxy;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractSingleComponentContainerConnector;
@@ -247,7 +247,7 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector
Style childStyle = layoutElement.getStyle();
// IE8 needs some hackery to measure its content correctly
- Util.forceIE8Redraw(layoutElement);
+ WidgetUtil.forceIE8Redraw(layoutElement);
if (content.isRelativeHeight() && !BrowserInfo.get().isIE9()) {
childStyle.setPosition(Position.ABSOLUTE);
diff --git a/client/src/com/vaadin/client/widget/escalator/Cell.java b/client/src/com/vaadin/client/widget/escalator/Cell.java
new file mode 100644
index 0000000000..08dbcf6955
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/Cell.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.google.gwt.dom.client.TableCellElement;
+
+/**
+ * Describes a cell
+ * <p>
+ * It's a representation of the element in a grid cell, and its row and column
+ * indices.
+ * <p>
+ * Unlike the {@link FlyweightRow}, an instance of {@link Cell} can be stored in
+ * a field.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Cell {
+
+ private final int row;
+
+ private final int column;
+
+ private final TableCellElement element;
+
+ /**
+ * Constructs a new {@link Cell}.
+ *
+ * @param row
+ * The index of the row
+ * @param column
+ * The index of the column
+ * @param element
+ * The cell element
+ */
+ public Cell(int row, int column, TableCellElement element) {
+ super();
+ this.row = row;
+ this.column = column;
+ this.element = element;
+ }
+
+ /**
+ * Returns the index of the row the cell resides in.
+ *
+ * @return the row index
+ *
+ */
+ public int getRow() {
+ return row;
+ }
+
+ /**
+ * Returns the index of the column the cell resides in.
+ *
+ * @return the column index
+ */
+ public int getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the element of the cell.
+ *
+ * @return the cell element
+ */
+ public TableCellElement getElement() {
+ return element;
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/ColumnConfiguration.java b/client/src/com/vaadin/client/widget/escalator/ColumnConfiguration.java
new file mode 100644
index 0000000000..af49dcd64f
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/ColumnConfiguration.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.vaadin.client.widgets.Escalator;
+
+/**
+ * A representation of the columns in an instance of {@link Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see Escalator#getColumnConfiguration()
+ */
+public interface ColumnConfiguration {
+
+ /**
+ * Removes columns at certain indices.
+ * <p>
+ * If any of the removed columns were frozen, the number of frozen columns
+ * will be reduced by the number of the removed columns that were frozen.
+ * <p>
+ * <em>Note:</em> This method simply removes the given columns, and does not
+ * do much of anything else. Especially if you have column spans, you
+ * probably need to run {@link #refreshColumns(int, int)} or
+ * {@link RowContainer#refreshRows(int, int)}
+ *
+ * @param index
+ * the index of the first column to be removed
+ * @param numberOfColumns
+ * the number of rows to remove, starting from {@code index}
+ * @throws IndexOutOfBoundsException
+ * if the entire range of removed columns is not currently
+ * present in the escalator
+ * @throws IllegalArgumentException
+ * if <code>numberOfColumns</code> is less than 1.
+ */
+ public void removeColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds columns at a certain index.
+ * <p>
+ * The new columns will be inserted between the column at the index, and the
+ * column before (an index of 0 means that the columns are inserted at the
+ * beginning). Therefore, the columns at the index and afterwards will be
+ * moved to the right.
+ * <p>
+ * The contents of the inserted columns will be queried from the respective
+ * cell renderers in the header, body and footer.
+ * <p>
+ * If there are frozen columns and the first added column is to the left of
+ * the last frozen column, the number of frozen columns will be increased by
+ * the number of inserted columns.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted columns will be
+ * rendered. If inserting new columns affects the contents of existing
+ * columns (e.g. you have column spans),
+ * {@link RowContainer#refreshRows(int, int)} or
+ * {@link #refreshColumns(int, int)} needs to be called as appropriate.
+ *
+ * @param index
+ * the index of the column before which new columns are inserted,
+ * or {@link #getColumnCount()} to add new columns at the end
+ * @param numberOfColumns
+ * the number of columns to insert after the <code>index</code>
+ * @throws IndexOutOfBoundsException
+ * if <code>index</code> is not an integer in the range
+ * <code>[0..{@link #getColumnCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfColumns} is less than 1.
+ */
+ public void insertColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Returns the number of columns in the escalator.
+ *
+ * @return the number of columns in the escalator
+ */
+ public int getColumnCount();
+
+ /**
+ * Sets the number of leftmost columns that are not affected by horizontal
+ * scrolling.
+ *
+ * @param count
+ * the number of columns to freeze
+ *
+ * @throws IllegalArgumentException
+ * if the column count is &lt; 0 or &gt; the number of columns
+ *
+ */
+ public void setFrozenColumnCount(int count) throws IllegalArgumentException;
+
+ /**
+ * Get the number of leftmost columns that are not affected by horizontal
+ * scrolling.
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount();
+
+ /**
+ * Sets (or unsets) an explicit width for a column.
+ *
+ * @param index
+ * the index of the column for which to set a width
+ * @param px
+ * the number of pixels the indicated column should be, or a
+ * negative number to let the escalator decide
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public void setColumnWidth(int index, double px)
+ throws IllegalArgumentException;
+
+ /**
+ * Returns the user-defined width of a column.
+ *
+ * @param index
+ * the index of the column for which to retrieve the width
+ * @return the column's width in pixels, or a negative number if the width
+ * is implicitly decided by the escalator
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public double getColumnWidth(int index) throws IllegalArgumentException;
+
+ /**
+ * Returns the actual width of a column.
+ *
+ * @param index
+ * the index of the column for which to retrieve the width
+ * @return the column's actual width in pixels
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public double getColumnWidthActual(int index)
+ throws IllegalArgumentException;
+
+ /**
+ * Refreshes a range of rows in the current row containers in each Escalator
+ * section.
+ * <p>
+ * The data for the refreshed columns is queried from the current cell
+ * renderer.
+ *
+ * @param index
+ * the index of the first row that will be updated
+ * @param numberOfRows
+ * the number of rows to update, starting from the index
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if {@code numberOfColumns} is less than 1.
+ * @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+ public void refreshColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java b/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java
new file mode 100644
index 0000000000..6109c5e51d
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.vaadin.client.widgets.Escalator;
+
+/**
+ * An interface that allows client code to define how a certain row in Escalator
+ * will be displayed. The contents of an escalator's header, body and footer are
+ * rendered by their respective updaters.
+ * <p>
+ * The updater is responsible for internally handling all remote communication,
+ * should the displayed data need to be fetched remotely.
+ * <p>
+ * This has a similar function to {@link Grid Grid's} {@link Renderer Renderers}
+ * , although they operate on different abstraction levels.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ * @see Renderer
+ */
+public interface EscalatorUpdater {
+
+ /**
+ * An {@link EscalatorUpdater} that doesn't render anything.
+ */
+ public static final EscalatorUpdater NULL = new EscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ // NOOP
+ }
+
+ @Override
+ public void preAttach(final Row row,
+ final Iterable<FlyweightCell> cellsToAttach) {
+ // NOOP
+
+ }
+
+ @Override
+ public void postAttach(final Row row,
+ final Iterable<FlyweightCell> attachedCells) {
+ // NOOP
+ }
+
+ @Override
+ public void preDetach(final Row row,
+ final Iterable<FlyweightCell> cellsToDetach) {
+ // NOOP
+ }
+
+ @Override
+ public void postDetach(final Row row,
+ final Iterable<FlyweightCell> detachedCells) {
+ // NOOP
+ }
+ };
+
+ /**
+ * Renders a row contained in a row container.
+ * <p>
+ * <em>Note:</em> If rendering of cells is deferred (e.g. because
+ * asynchronous data retrieval), this method is responsible for explicitly
+ * displaying some placeholder data (empty content is valid). Because the
+ * cells (and rows) in an escalator are recycled, failing to reset a cell's
+ * presentation will lead to wrong data being displayed in the escalator.
+ * <p>
+ * For performance reasons, the escalator will never autonomously clear any
+ * data in a cell.
+ *
+ * @param row
+ * Information about the row that is being updated.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param cellsToUpdate
+ * A collection of cells that need to be updated. <em>Note:</em>
+ * You should neither store nor reuse the reference to the
+ * iterable, nor to the individual cells.
+ */
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate);
+
+ /**
+ * Called before attaching new cells to the escalator.
+ *
+ * @param row
+ * Information about the row to which the cells will be added.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param cellsToAttach
+ * A collection of cells that are about to be attached.
+ * <em>Note:</em> You should neither store nor reuse the
+ * reference to the iterable, nor to the individual cells.
+ *
+ */
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach);
+
+ /**
+ * Called after attaching new cells to the escalator.
+ *
+ * @param row
+ * Information about the row to which the cells were added.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param attachedCells
+ * A collection of cells that were attached. <em>Note:</em> You
+ * should neither store nor reuse the reference to the iterable,
+ * nor to the individual cells.
+ *
+ */
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells);
+
+ /**
+ * Called before detaching cells from the escalator.
+ *
+ * @param row
+ * Information about the row from which the cells will be
+ * removed. <em>Note:</em> You should not store nor reuse this
+ * reference.
+ * @param cellsToAttach
+ * A collection of cells that are about to be detached.
+ * <em>Note:</em> You should neither store nor reuse the
+ * reference to the iterable, nor to the individual cells.
+ *
+ */
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach);
+
+ /**
+ * Called after detaching cells from the escalator.
+ *
+ * @param row
+ * Information about the row from which the cells were removed.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param attachedCells
+ * A collection of cells that were detached. <em>Note:</em> You
+ * should neither store nor reuse the reference to the iterable,
+ * nor to the individual cells.
+ *
+ */
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells);
+
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/FlyweightCell.java b/client/src/com/vaadin/client/widget/escalator/FlyweightCell.java
new file mode 100644
index 0000000000..b77b752327
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/FlyweightCell.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import java.util.List;
+
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.client.widget.escalator.FlyweightRow.CellIterator;
+import com.vaadin.client.widgets.Escalator;
+
+/**
+ * A {@link FlyweightCell} represents a cell in the {@link Grid} or
+ * {@link Escalator} at a certain point in time.
+ *
+ * <p>
+ * Since the {@link FlyweightCell} follows the <code>Flyweight</code>-pattern
+ * any instance of this object is subject to change without the user knowing it
+ * and so should not be stored anywhere outside of the method providing these
+ * instances.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class FlyweightCell {
+ public static final String COLSPAN_ATTR = "colSpan";
+
+ private final int column;
+ private final FlyweightRow row;
+
+ private TableCellElement element = null;
+ private CellIterator currentIterator = null;
+
+ public FlyweightCell(final FlyweightRow row, final int column) {
+ this.row = row;
+ this.column = column;
+ }
+
+ /**
+ * Returns the row index of the cell
+ */
+ public int getRow() {
+ assertSetup();
+ return row.getRow();
+ }
+
+ /**
+ * Returns the column index of the cell
+ */
+ public int getColumn() {
+ assertSetup();
+ return column;
+ }
+
+ /**
+ * Returns the element of the cell. Can be either a <code>TD</code> element
+ * or a <code>TH</code> element.
+ */
+ public TableCellElement getElement() {
+ assertSetup();
+ return element;
+ }
+
+ /**
+ * Return the colspan attribute of the element of the cell.
+ */
+ public int getColSpan() {
+ assertSetup();
+ return element.getPropertyInt(COLSPAN_ATTR);
+ }
+
+ /**
+ * Sets the DOM element for this FlyweightCell, either a <code>TD</code> or
+ * a <code>TH</code>. It is the caller's responsibility to actually insert
+ * the given element to the document when needed.
+ *
+ * @param element
+ * the element corresponding to this cell, cannot be null
+ */
+ public void setElement(TableCellElement element) {
+ assert element != null;
+ assertSetup();
+ this.element = element;
+ }
+
+ void setup(final CellIterator iterator) {
+ currentIterator = iterator;
+
+ if (iterator.areCellsAttached()) {
+ final TableCellElement e = row.getElement().getCells()
+ .getItem(column);
+
+ assert e != null : "Cell " + column + " for logical row "
+ + row.getRow() + " doesn't exist in the DOM!";
+
+ e.setPropertyInt(COLSPAN_ATTR, 1);
+ if (row.getColumnWidth(column) >= 0) {
+ e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX);
+ }
+ e.getStyle().clearDisplay();
+ setElement(e);
+ }
+ }
+
+ /**
+ * Tear down the state of the Cell.
+ * <p>
+ * This is an internal check method, to prevent retrieving uninitialized
+ * data by calling {@link #getRow()}, {@link #getColumn()} or
+ * {@link #getElement()} at an improper time.
+ * <p>
+ * This should only be used with asserts ("
+ * <code>assert flyweightCell.teardown()</code> ") so that the code is never
+ * run when asserts aren't enabled.
+ *
+ * @return always <code>true</code>
+ * @see FlyweightRow#teardown()
+ */
+ boolean teardown() {
+ currentIterator = null;
+ element = null;
+ return true;
+ }
+
+ /**
+ * Asserts that the flyweight cell has properly been set up before trying to
+ * access any of its data.
+ */
+ private void assertSetup() {
+ assert currentIterator != null : "FlyweightCell was not properly "
+ + "initialized. This is either a bug in Grid/Escalator "
+ + "or a Cell reference has been stored and reused "
+ + "inappropriately.";
+ }
+
+ public void setColSpan(final int numberOfCells) {
+ if (numberOfCells < 1) {
+ throw new IllegalArgumentException(
+ "Number of cells should be more than 0");
+ }
+
+ /*-
+ * This will default to 1 if unset, as per DOM specifications:
+ * http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements
+ */
+ final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR);
+ if (numberOfCells == 1 && prevColSpan == 1) {
+ return;
+ }
+
+ getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells);
+ adjustCellWidthForSpan(numberOfCells);
+ hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan);
+ currentIterator.setSkipNext(numberOfCells - 1);
+ }
+
+ private void adjustCellWidthForSpan(final int numberOfCells) {
+ final int cellsToTheRight = currentIterator.rawPeekNext(
+ numberOfCells - 1).size();
+
+ final double selfWidth = row.getColumnWidth(column);
+ double widthsOfColumnsToTheRight = 0;
+ for (int i = 0; i < cellsToTheRight; i++) {
+ widthsOfColumnsToTheRight += row.getColumnWidth(column + i + 1);
+ }
+ getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight,
+ Unit.PX);
+ }
+
+ private void hideOrRevealAdjacentCellElements(final int numberOfCells,
+ final int prevColSpan) {
+ final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells);
+ final List<FlyweightCell> affectedCells = currentIterator
+ .rawPeekNext(affectedCellsNumber - 1);
+ if (prevColSpan < numberOfCells) {
+ for (int i = 0; i < affectedCells.size(); i++) {
+ affectedCells.get(prevColSpan + i - 1).getElement().getStyle()
+ .setDisplay(Display.NONE);
+ }
+ } else if (prevColSpan > numberOfCells) {
+ for (int i = 0; i < affectedCells.size(); i++) {
+ affectedCells.get(numberOfCells + i - 1).getElement()
+ .getStyle().clearDisplay();
+ }
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/FlyweightRow.java b/client/src/com/vaadin/client/widget/escalator/FlyweightRow.java
new file mode 100644
index 0000000000..6e25e82235
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/FlyweightRow.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.client.widgets.Escalator;
+
+/**
+ * An internal implementation of the {@link Row} interface.
+ * <p>
+ * There is only one instance per Escalator. This is designed to be re-used when
+ * rendering rows.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see Escalator.AbstractRowContainer#refreshRow(Node, int)
+ */
+public class FlyweightRow implements Row {
+
+ static class CellIterator implements Iterator<FlyweightCell> {
+ /** A defensive copy of the cells in the current row. */
+ private final ArrayList<FlyweightCell> cells;
+ private final boolean cellsAttached;
+ private int cursor = 0;
+ private int skipNext = 0;
+
+ /**
+ * Creates a new iterator of attached flyweight cells. A cell is
+ * attached if it has a corresponding {@link FlyweightCell#getElement()
+ * DOM element} attached to the row element.
+ *
+ * @param cells
+ * the collection of cells to iterate
+ */
+ public static CellIterator attached(
+ final Collection<FlyweightCell> cells) {
+ return new CellIterator(cells, true);
+ }
+
+ /**
+ * Creates a new iterator of unattached flyweight cells. A cell is
+ * unattached if it does not have a corresponding
+ * {@link FlyweightCell#getElement() DOM element} attached to the row
+ * element.
+ *
+ * @param cells
+ * the collection of cells to iterate
+ */
+ public static CellIterator unattached(
+ final Collection<FlyweightCell> cells) {
+ return new CellIterator(cells, false);
+ }
+
+ private CellIterator(final Collection<FlyweightCell> cells,
+ final boolean attached) {
+ this.cells = new ArrayList<FlyweightCell>(cells);
+ cellsAttached = attached;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return cursor + skipNext < cells.size();
+ }
+
+ @Override
+ public FlyweightCell next() {
+ // if we needed to skip some cells since the last invocation.
+ for (int i = 0; i < skipNext; i++) {
+ cells.remove(cursor);
+ }
+ skipNext = 0;
+
+ final FlyweightCell cell = cells.get(cursor++);
+ cell.setup(this);
+ return cell;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Cannot remove cells via iterator");
+ }
+
+ /**
+ * Sets the number of cells to skip when {@link #next()} is called the
+ * next time. Cell hiding is also handled eagerly in this method.
+ *
+ * @param colspan
+ * the number of cells to skip on next invocation of
+ * {@link #next()}
+ */
+ public void setSkipNext(final int colspan) {
+ assert colspan > 0 : "Number of cells didn't make sense: "
+ + colspan;
+ skipNext = colspan;
+ }
+
+ /**
+ * Gets the next <code>n</code> cells in the iterator, ignoring any
+ * possibly spanned cells.
+ *
+ * @param n
+ * the number of next cells to retrieve
+ * @return A list of next <code>n</code> cells, or less if there aren't
+ * enough cells to retrieve
+ */
+ public List<FlyweightCell> rawPeekNext(final int n) {
+ final int from = Math.min(cursor, cells.size());
+ final int to = Math.min(cursor + n, cells.size());
+ List<FlyweightCell> nextCells = cells.subList(from, to);
+ for (FlyweightCell cell : nextCells) {
+ cell.setup(this);
+ }
+ return nextCells;
+ }
+
+ public boolean areCellsAttached() {
+ return cellsAttached;
+ }
+ }
+
+ private static final int BLANK = Integer.MIN_VALUE;
+
+ private int row;
+ private TableRowElement element;
+ private double[] columnWidths = null;
+ private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>();
+
+ public void setup(final TableRowElement e, final int row,
+ double[] columnWidths) {
+ element = e;
+ this.row = row;
+ this.columnWidths = columnWidths;
+ }
+
+ /**
+ * Tear down the state of the Row.
+ * <p>
+ * This is an internal check method, to prevent retrieving uninitialized
+ * data by calling {@link #getRow()}, {@link #getElement()} or
+ * {@link #getCells()} at an improper time.
+ * <p>
+ * This should only be used with asserts ("
+ * <code>assert flyweightRow.teardown()</code> ") so that the code is never
+ * run when asserts aren't enabled.
+ *
+ * @return always <code>true</code>
+ */
+ public boolean teardown() {
+ element = null;
+ row = BLANK;
+ columnWidths = null;
+ for (final FlyweightCell cell : cells) {
+ assert cell.teardown();
+ }
+ return true;
+ }
+
+ @Override
+ public int getRow() {
+ assertSetup();
+ return row;
+ }
+
+ @Override
+ public TableRowElement getElement() {
+ assertSetup();
+ return element;
+ }
+
+ public void addCells(final int index, final int numberOfColumns) {
+ for (int i = 0; i < numberOfColumns; i++) {
+ final int col = index + i;
+ cells.add(col, new FlyweightCell(this, col));
+ }
+ updateRestOfCells(index + numberOfColumns);
+ }
+
+ public void removeCells(final int index, final int numberOfColumns) {
+ cells.subList(index, index + numberOfColumns).clear();
+ updateRestOfCells(index);
+ }
+
+ private void updateRestOfCells(final int startPos) {
+ // update the column number for the cells to the right
+ for (int col = startPos; col < cells.size(); col++) {
+ cells.set(col, new FlyweightCell(this, col));
+ }
+ }
+
+ /**
+ * Returns flyweight cells for the client code to render. The cells get
+ * their associated {@link FlyweightCell#getElement() elements} from the row
+ * element.
+ * <p>
+ * Precondition: each cell has a corresponding element in the row
+ *
+ * @return an iterable of flyweight cells
+ *
+ * @see #setup(Element, int, int[])
+ * @see #teardown()
+ */
+ public Iterable<FlyweightCell> getCells() {
+ return getCells(0, cells.size());
+ }
+
+ /**
+ * Returns a subrange of flyweight cells for the client code to render. The
+ * cells get their associated {@link FlyweightCell#getElement() elements}
+ * from the row element.
+ * <p>
+ * Precondition: each cell has a corresponding element in the row
+ *
+ * @param offset
+ * the index of the first cell to return
+ * @param numberOfCells
+ * the number of cells to return
+ * @return an iterable of flyweight cells
+ */
+ public Iterable<FlyweightCell> getCells(final int offset,
+ final int numberOfCells) {
+ assertSetup();
+ assert offset >= 0 && offset + numberOfCells <= cells.size() : "Invalid range of cells";
+ return new Iterable<FlyweightCell>() {
+ @Override
+ public Iterator<FlyweightCell> iterator() {
+ return CellIterator.attached(cells.subList(offset, offset
+ + numberOfCells));
+ }
+ };
+ }
+
+ /**
+ * Returns a subrange of unattached flyweight cells. Unattached cells do not
+ * have {@link FlyweightCell#getElement() elements} associated. Note that
+ * FlyweightRow does not keep track of whether cells in actuality have
+ * corresponding DOM elements or not; it is the caller's responsibility to
+ * invoke this method with correct parameters.
+ * <p>
+ * Precondition: the range [offset, offset + numberOfCells) must be valid
+ *
+ * @param offset
+ * the index of the first cell to return
+ * @param numberOfCells
+ * the number of cells to return
+ * @return an iterable of flyweight cells
+ */
+ public Iterable<FlyweightCell> getUnattachedCells(final int offset,
+ final int numberOfCells) {
+ assertSetup();
+ assert offset >= 0 && offset + numberOfCells <= cells.size() : "Invalid range of cells";
+ return new Iterable<FlyweightCell>() {
+ @Override
+ public Iterator<FlyweightCell> iterator() {
+ return CellIterator.unattached(cells.subList(offset, offset
+ + numberOfCells));
+ }
+ };
+ }
+
+ /**
+ * Asserts that the flyweight row has properly been set up before trying to
+ * access any of its data.
+ */
+ private void assertSetup() {
+ assert element != null && row != BLANK && columnWidths != null : "Flyweight row was not "
+ + "properly initialized. Make sure the setup-method is "
+ + "called before retrieving data. This is either a bug "
+ + "in Escalator, or the instance of the flyweight row "
+ + "has been stored and accessed.";
+ }
+
+ double getColumnWidth(int column) {
+ assertSetup();
+ return columnWidths[column];
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/PositionFunction.java b/client/src/com/vaadin/client/widget/escalator/PositionFunction.java
new file mode 100644
index 0000000000..929f27df37
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/PositionFunction.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
+
+/**
+ * A functional interface that can be used for positioning elements in the DOM.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface PositionFunction {
+ /**
+ * A position function using "transform: translate3d(x,y,z)" to position
+ * elements in the DOM.
+ */
+ public static class Translate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate3d(" + x + "px, " + y + "px, 0)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("transform");
+ }
+ }
+
+ /**
+ * A position function using "transform: translate(x,y)" to position
+ * elements in the DOM.
+ */
+ public static class TranslatePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate(" + x + "px," + y + "px)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("transform");
+ }
+ }
+
+ /**
+ * A position function using "-webkit-transform: translate3d(x,y,z)" to
+ * position elements in the DOM.
+ */
+ public static class WebkitTranslate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("webkitTransform",
+ "translate3d(" + x + "px," + y + "px,0)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("webkitTransform");
+ }
+ }
+
+ /**
+ * A position function using "left: x" and "top: y" to position elements in
+ * the DOM.
+ */
+ public static class AbsolutePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setLeft(x, Unit.PX);
+ e.getStyle().setTop(y, Unit.PX);
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearLeft();
+ e.getStyle().clearTop();
+ }
+ }
+
+ /**
+ * Position an element in an (x,y) coordinate system in the DOM.
+ *
+ * @param e
+ * the element to position. Never <code>null</code>.
+ * @param x
+ * the x coordinate, in pixels
+ * @param y
+ * the y coordinate, in pixels
+ */
+ void set(Element e, double x, double y);
+
+ /**
+ * Resets any previously applied positioning, clearing the used style
+ * attributes.
+ *
+ * @param e
+ * the element for which to reset the positioning
+ */
+ void reset(Element e);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/escalator/Row.java b/client/src/com/vaadin/client/widget/escalator/Row.java
new file mode 100644
index 0000000000..bcb3e163e4
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/Row.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.client.widgets.Escalator;
+
+/**
+ * A representation of a row in an {@link Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Row {
+ /**
+ * Gets the row index.
+ *
+ * @return the row index
+ */
+ public int getRow();
+
+ /**
+ * Gets the root element for this row.
+ * <p>
+ * The {@link EscalatorUpdater} may update the class names of the element
+ * and add inline styles, but may not modify the contained DOM structure.
+ * <p>
+ * If you wish to modify the cells within this row element, access them via
+ * the <code>List&lt;{@link Cell}&gt;</code> objects passed in to
+ * {@code EscalatorUpdater.updateCells(Row, List)}
+ *
+ * @return the root element of the row
+ */
+ public TableRowElement getElement();
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java
new file mode 100644
index 0000000000..2fe2070b0d
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.client.widgets.Escalator;
+
+/**
+ * A representation of the rows in each of the sections (header, body and
+ * footer) in an {@link Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+public interface RowContainer {
+
+ /**
+ * An arbitrary pixel height of a row, before any autodetection for the row
+ * height has been made.
+ * */
+ public static final double INITIAL_DEFAULT_ROW_HEIGHT = 20;
+
+ /**
+ * Returns the current {@link EscalatorUpdater} used to render cells.
+ *
+ * @return the current escalator updater
+ */
+ public EscalatorUpdater getEscalatorUpdater();
+
+ /**
+ * Sets the {@link EscalatorUpdater} to use when displaying data in the
+ * escalator.
+ *
+ * @param escalatorUpdater
+ * the escalator updater to use to render cells. May not be
+ * <code>null</code>
+ * @throws IllegalArgumentException
+ * if {@code cellRenderer} is <code>null</code>
+ * @see EscalatorUpdater#NULL
+ */
+ public void setEscalatorUpdater(EscalatorUpdater escalatorUpdater)
+ throws IllegalArgumentException;
+
+ /**
+ * Removes rows at a certain index in the current row container.
+ *
+ * @param index
+ * the index of the first row to be removed
+ * @param numberOfRows
+ * the number of rows to remove, starting from the index
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfRows)]</code> is not an existing
+ * row index
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void removeRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds rows at a certain index in this row container.
+ * <p>
+ * The new rows will be inserted between the row at the index, and the row
+ * before (an index of 0 means that the rows are inserted at the beginning).
+ * Therefore, the rows currently at the index and afterwards will be moved
+ * downwards.
+ * <p>
+ * The contents of the inserted rows will subsequently be queried from the
+ * escalator updater.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted rows will be rendered.
+ * If inserting new rows affects the contents of existing rows,
+ * {@link #refreshRows(int, int)} needs to be called for those rows
+ * separately.
+ *
+ * @param index
+ * the index of the row before which new rows are inserted, or
+ * {@link #getRowCount()} to add rows at the end
+ * @param numberOfRows
+ * the number of rows to insert after the <code>index</code>
+ * @see #setEscalatorUpdater(EscalatorUpdater)
+ * @throws IndexOutOfBoundsException
+ * if <code>index</code> is not an integer in the range
+ * <code>[0..{@link #getRowCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void insertRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Refreshes a range of rows in the current row container.
+ * <p>
+ * The data for the refreshed rows is queried from the current cell
+ * renderer.
+ *
+ * @param index
+ * the index of the first row that will be updated
+ * @param numberOfRows
+ * the number of rows to update, starting from the index
+ * @see #setEscalatorUpdater(EscalatorUpdater)
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void refreshRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Gets the number of rows in the current row container.
+ *
+ * @return the number of rows in the current row container
+ */
+ public int getRowCount();
+
+ /**
+ * The default height of the rows in this RowContainer.
+ *
+ * @param px
+ * the default height in pixels of the rows in this RowContainer
+ * @throws IllegalArgumentException
+ * if <code>px &lt; 1</code>
+ * @see #getDefaultRowHeight()
+ */
+ public void setDefaultRowHeight(double px) throws IllegalArgumentException;
+
+ /**
+ * Returns the default height of the rows in this RowContainer.
+ * <p>
+ * This value will be equal to {@link #INITIAL_DEFAULT_ROW_HEIGHT} if the
+ * {@link Escalator} has not yet had a chance to autodetect the row height,
+ * or no explicit value has yet given via {@link #setDefaultRowHeight(int)}
+ *
+ * @return the default height of the rows in this RowContainer, in pixels
+ * @see #setDefaultRowHeight(int)
+ */
+ public double getDefaultRowHeight();
+
+ /**
+ * Returns the cell object which contains information about the cell the
+ * element is in.
+ *
+ * @param element
+ * The element to get the cell for. If element is not present in
+ * row container then <code>null</code> is returned.
+ *
+ * @return the cell of the element, or <code>null</code> if element is not
+ * present in the {@link RowContainer}.
+ */
+ public Cell getCell(Element element);
+
+ /**
+ * Gets the row element with given logical index. For lazy loaded containers
+ * such as Escalators BodyRowContainer visibility should be checked before
+ * calling this function. See {@link Escalator#getVisibleRowRange()}.
+ *
+ * @param index
+ * the logical index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within container
+ * @throws IllegalStateException
+ * if {@code index} is currently not available in the DOM
+ */
+ public TableRowElement getRowElement(int index)
+ throws IndexOutOfBoundsException, IllegalStateException;
+
+ /**
+ * Returns the root element of RowContainer
+ *
+ * @return RowContainer root element
+ */
+ public Element getElement();
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java b/client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java
new file mode 100644
index 0000000000..968013b401
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * Event fired when the range of visible rows changes e.g. because of scrolling.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RowVisibilityChangeEvent extends
+ GwtEvent<RowVisibilityChangeHandler> {
+ /**
+ * The type of this event.
+ */
+ public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>();
+
+ private final int firstVisibleRow;
+ private final int visibleRowCount;
+
+ /**
+ * Creates a new row visibility change event
+ *
+ * @param firstVisibleRow
+ * the index of the first visible row
+ * @param visibleRowCount
+ * the number of visible rows
+ */
+ public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) {
+ this.firstVisibleRow = firstVisibleRow;
+ this.visibleRowCount = visibleRowCount;
+ }
+
+ /**
+ * Gets the index of the first row that is at least partially visible.
+ *
+ * @return the index of the first visible row
+ */
+ public int getFirstVisibleRow() {
+ return firstVisibleRow;
+ }
+
+ /**
+ * Gets the number of at least partially visible rows.
+ *
+ * @return the number of visible rows
+ */
+ public int getVisibleRowCount() {
+ return visibleRowCount;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType()
+ */
+ @Override
+ public Type<RowVisibilityChangeHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared
+ * .EventHandler)
+ */
+ @Override
+ protected void dispatch(RowVisibilityChangeHandler handler) {
+ handler.onRowVisibilityChange(this);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeHandler.java b/client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeHandler.java
new file mode 100644
index 0000000000..80a30184c0
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/RowVisibilityChangeHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Event handler that gets notified when the range of visible rows changes e.g.
+ * because of scrolling.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface RowVisibilityChangeHandler extends EventHandler {
+
+ /**
+ * Called when the range of visible rows changes e.g. because of scrolling.
+ *
+ * @param event
+ * the row visibility change event describing the change
+ */
+ void onRowVisibilityChange(RowVisibilityChangeEvent event);
+
+}
diff --git a/client/src/com/vaadin/client/widget/escalator/ScrollbarBundle.java b/client/src/com/vaadin/client/widget/escalator/ScrollbarBundle.java
new file mode 100644
index 0000000000..d7122329b7
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/escalator/ScrollbarBundle.java
@@ -0,0 +1,854 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.escalator;
+
+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.Display;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+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.EventListener;
+import com.google.gwt.user.client.Timer;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+
+/**
+ * An element-like bundle representing a configurable and visual scrollbar in
+ * one axis.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see VerticalScrollbarBundle
+ * @see HorizontalScrollbarBundle
+ */
+public abstract class ScrollbarBundle implements DeferredWorker {
+
+ private class ScrollEventFirer {
+ private final ScheduledCommand fireEventCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+
+ /*
+ * Some kind of native-scroll-event related asynchronous problem
+ * occurs here (at least on desktops) where the internal
+ * bookkeeping isn't up to date with the real scroll position.
+ * The weird thing is, that happens only once, and if you drag
+ * scrollbar fast enough. After it has failed once, it never
+ * fails again.
+ *
+ * Theory: the user drags the scrollbar, and this command is
+ * executed before the browser has a chance to fire a scroll
+ * event (which normally would correct this situation). This
+ * would explain why slow scrolling doesn't trigger the problem,
+ * while fast scrolling does.
+ *
+ * To make absolutely sure that we have the latest scroll
+ * position, let's update the internal value.
+ *
+ * This might lead to a slight performance hit (on my computer
+ * it was never more than 3ms on either of Chrome 38 or Firefox
+ * 31). It also _slightly_ counteracts the purpose of the
+ * internal bookkeeping. But since getScrollPos is called 3
+ * times (on one direction) per scroll loop, it's still better
+ * to have take this small penalty than removing it altogether.
+ */
+ updateScrollPosFromDom();
+
+ getHandlerManager().fireEvent(new ScrollEvent());
+ isBeingFired = false;
+ }
+ };
+
+ private boolean isBeingFired;
+
+ public void scheduleEvent() {
+ if (!isBeingFired) {
+ /*
+ * We'll gather all the scroll events, and only fire once, once
+ * everything has calmed down.
+ */
+ Scheduler.get().scheduleDeferred(fireEventCommand);
+ isBeingFired = true;
+ }
+ }
+ }
+
+ /**
+ * The orientation of the scrollbar.
+ */
+ public enum Direction {
+ VERTICAL, HORIZONTAL;
+ }
+
+ private class TemporaryResizer extends Object {
+ private static final int TEMPORARY_RESIZE_DELAY = 1000;
+
+ private final Timer timer = new Timer() {
+ @Override
+ public void run() {
+ internalSetScrollbarThickness(1);
+ }
+ };
+
+ public void show() {
+ internalSetScrollbarThickness(OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
+ timer.schedule(TEMPORARY_RESIZE_DELAY);
+ }
+ }
+
+ /**
+ * A means to listen to when the scrollbar handle in a
+ * {@link ScrollbarBundle} either appears or is removed.
+ */
+ public interface VisibilityHandler extends EventHandler {
+ /**
+ * This method is called whenever the scrollbar handle's visibility is
+ * changed in a {@link ScrollbarBundle}.
+ *
+ * @param event
+ * the {@link VisibilityChangeEvent}
+ */
+ void visibilityChanged(VisibilityChangeEvent event);
+ }
+
+ public static class VisibilityChangeEvent extends
+ GwtEvent<VisibilityHandler> {
+ public static final Type<VisibilityHandler> TYPE = new Type<ScrollbarBundle.VisibilityHandler>() {
+ @Override
+ public String toString() {
+ return "VisibilityChangeEvent";
+ }
+ };
+
+ private final boolean isScrollerVisible;
+
+ private VisibilityChangeEvent(boolean isScrollerVisible) {
+ this.isScrollerVisible = isScrollerVisible;
+ }
+
+ /**
+ * Checks whether the scroll handle is currently visible or not
+ *
+ * @return <code>true</code> if the scroll handle is currently visible.
+ * <code>false</code> if not.
+ */
+ public boolean isScrollerVisible() {
+ return isScrollerVisible;
+ }
+
+ @Override
+ public Type<VisibilityHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(VisibilityHandler handler) {
+ handler.visibilityChanged(this);
+ }
+ }
+
+ /**
+ * The pixel size for OSX's invisible scrollbars.
+ * <p>
+ * Touch devices don't show a scrollbar at all, so the scrollbar size is
+ * irrelevant in their case. There doesn't seem to be any other popular
+ * platforms that has scrollbars similar to OSX. Thus, this behavior is
+ * tailored for OSX only, until additional platforms start behaving this
+ * way.
+ */
+ private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;
+
+ /**
+ * A representation of a single vertical scrollbar.
+ *
+ * @see VerticalScrollbarBundle#getElement()
+ */
+ public final static class VerticalScrollbarBundle extends ScrollbarBundle {
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ root.addClassName(primaryStyleName + "-scroller-vertical");
+ }
+
+ @Override
+ protected void internalSetScrollPos(int px) {
+ root.setScrollTop(px);
+ }
+
+ @Override
+ protected int internalGetScrollPos() {
+ return root.getScrollTop();
+ }
+
+ @Override
+ protected void internalSetScrollSize(double px) {
+ scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollSize() {
+ return scrollSizeElement.getStyle().getHeight();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(double px) {
+ root.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ public String internalGetOffsetSize() {
+ return root.getStyle().getHeight();
+ }
+
+ @Override
+ protected void internalSetScrollbarThickness(double px) {
+ root.getStyle().setWidth(px, Unit.PX);
+ scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollbarThickness() {
+ return root.getStyle().getWidth();
+ }
+
+ @Override
+ protected void internalForceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().setOverflowY(Overflow.SCROLL);
+ } else {
+ root.getStyle().clearOverflowY();
+ }
+ }
+
+ @Override
+ public Direction getDirection() {
+ return Direction.VERTICAL;
+ }
+ }
+
+ /**
+ * A representation of a single horizontal scrollbar.
+ *
+ * @see HorizontalScrollbarBundle#getElement()
+ */
+ public final static class HorizontalScrollbarBundle extends ScrollbarBundle {
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ root.addClassName(primaryStyleName + "-scroller-horizontal");
+ }
+
+ @Override
+ protected void internalSetScrollPos(int px) {
+ root.setScrollLeft(px);
+ }
+
+ @Override
+ protected int internalGetScrollPos() {
+ return root.getScrollLeft();
+ }
+
+ @Override
+ protected void internalSetScrollSize(double px) {
+ scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollSize() {
+ return scrollSizeElement.getStyle().getWidth();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(double px) {
+ root.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ public String internalGetOffsetSize() {
+ return root.getStyle().getWidth();
+ }
+
+ @Override
+ protected void internalSetScrollbarThickness(double px) {
+ root.getStyle().setHeight(px, Unit.PX);
+ scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ protected String internalGetScrollbarThickness() {
+ return root.getStyle().getHeight();
+ }
+
+ @Override
+ protected void internalForceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().setOverflowX(Overflow.SCROLL);
+ } else {
+ root.getStyle().clearOverflowX();
+ }
+ }
+
+ @Override
+ public Direction getDirection() {
+ return Direction.HORIZONTAL;
+ }
+ }
+
+ protected final Element root = DOM.createDiv();
+ protected final Element scrollSizeElement = DOM.createDiv();
+ protected boolean isInvisibleScrollbar = false;
+
+ private double scrollPos = 0;
+ private double maxScrollPos = 0;
+
+ private boolean scrollHandleIsVisible = false;
+
+ private boolean isLocked = false;
+
+ /** @deprecarted access via {@link #getHandlerManager()} instead. */
+ @Deprecated
+ private HandlerManager handlerManager;
+
+ private TemporaryResizer invisibleScrollbarTemporaryResizer = new TemporaryResizer();
+
+ private final ScrollEventFirer scrollEventFirer = new ScrollEventFirer();
+
+ private HandlerRegistration scrollSizeTemporaryScrollHandler;
+ private HandlerRegistration offsetSizeTemporaryScrollHandler;
+
+ private ScrollbarBundle() {
+ root.appendChild(scrollSizeElement);
+ root.getStyle().setDisplay(Display.NONE);
+ root.setTabIndex(-1);
+ }
+
+ protected abstract String internalGetScrollSize();
+
+ /**
+ * Sets the primary style name
+ *
+ * @param primaryStyleName
+ * The primary style name to use
+ */
+ public void setStylePrimaryName(String primaryStyleName) {
+ root.setClassName(primaryStyleName + "-scroller");
+ }
+
+ /**
+ * Gets the root element of this scrollbar-composition.
+ *
+ * @return the root element
+ */
+ public final Element getElement() {
+ return root;
+ }
+
+ /**
+ * Modifies the scroll position of this scrollbar by a number of pixels.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param delta
+ * the delta in pixels to change the scroll position by
+ */
+ public final void setScrollPosByDelta(double delta) {
+ if (delta != 0) {
+ setScrollPos(getScrollPos() + delta);
+ }
+ }
+
+ /**
+ * Modifies {@link #root root's} dimensions in the axis the scrollbar is
+ * representing.
+ *
+ * @param px
+ * the new size of {@link #root} in the dimension this scrollbar
+ * is representing
+ */
+ protected abstract void internalSetOffsetSize(double px);
+
+ /**
+ * Sets the length of the scrollbar.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the length of the scrollbar in pixels
+ */
+ public final void setOffsetSize(final double px) {
+
+ /*
+ * This needs to be made step-by-step because IE8 flat-out refuses to
+ * fire a scroll event when the scroll size becomes smaller than the
+ * offset size. All other browser need to suffer alongside.
+ */
+
+ boolean newOffsetSizeIsGreaterThanScrollSize = px > getScrollSize();
+ boolean offsetSizeBecomesGreaterThanScrollSize = showsScrollHandle()
+ && newOffsetSizeIsGreaterThanScrollSize;
+ if (offsetSizeBecomesGreaterThanScrollSize && getScrollPos() != 0) {
+ // must be a field because Java insists.
+ offsetSizeTemporaryScrollHandler = addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ setOffsetSizeNow(px);
+ offsetSizeTemporaryScrollHandler.removeHandler();
+ offsetSizeTemporaryScrollHandler = null;
+ }
+ });
+ setScrollPos(0);
+ } else {
+ setOffsetSizeNow(px);
+ }
+ }
+
+ private void setOffsetSizeNow(double px) {
+ internalSetOffsetSize(Math.max(0, truncate(px)));
+ recalculateMaxScrollPos();
+ forceScrollbar(showsScrollHandle());
+ fireVisibilityChangeIfNeeded();
+ }
+
+ /**
+ * Force the scrollbar to be visible with CSS. In practice, this means to
+ * set either <code>overflow-x</code> or <code>overflow-y</code> to "
+ * <code>scroll</code>" in the scrollbar's direction.
+ * <p>
+ * This is an IE8 workaround, since it doesn't always show scrollbars with
+ * <code>overflow: auto</code> enabled.
+ */
+ protected void forceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().clearDisplay();
+ } else {
+ root.getStyle().setDisplay(Display.NONE);
+ }
+ internalForceScrollbar(enable);
+ }
+
+ protected abstract void internalForceScrollbar(boolean enable);
+
+ /**
+ * Gets the length of the scrollbar
+ *
+ * @return the length of the scrollbar in pixels
+ */
+ public double getOffsetSize() {
+ return parseCssDimensionToPixels(internalGetOffsetSize());
+ }
+
+ public abstract String internalGetOffsetSize();
+
+ /**
+ * Sets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the new scroll position in pixels
+ */
+ public final void setScrollPos(double px) {
+ if (isLocked()) {
+ return;
+ }
+
+ double oldScrollPos = scrollPos;
+ scrollPos = Math.max(0, Math.min(maxScrollPos, truncate(px)));
+
+ if (!WidgetUtil.pixelValuesEqual(oldScrollPos, scrollPos)) {
+ if (isInvisibleScrollbar) {
+ invisibleScrollbarTemporaryResizer.show();
+ }
+
+ /*
+ * This is where the value needs to be converted into an integer no
+ * matter how we flip it, since GWT expects an integer value.
+ * There's no point making a JSNI method that accepts doubles as the
+ * scroll position, since the browsers themselves don't support such
+ * large numbers (as of today, 25.3.2014). This double-ranged is
+ * only facilitating future virtual scrollbars.
+ */
+ internalSetScrollPos(toInt32(scrollPos));
+ }
+ }
+
+ /**
+ * Truncates a double such that no decimal places are retained.
+ * <p>
+ * E.g. {@code trunc(2.3d) == 2.0d} and {@code trunc(-2.3d) == -2.0d}.
+ *
+ * @param num
+ * the double value to be truncated
+ * @return the {@code num} value without any decimal digits
+ */
+ private static double truncate(double num) {
+ if (num > 0) {
+ return Math.floor(num);
+ } else {
+ return Math.ceil(num);
+ }
+ }
+
+ /**
+ * Modifies the element's scroll position (scrollTop or scrollLeft).
+ * <p>
+ * <em>Note:</em> The parameter here is a type of integer (instead of a
+ * double) by design. The browsers internally convert all double values into
+ * an integer value. To make this fact explicit, this API has chosen to
+ * force integers already at this level.
+ *
+ * @param px
+ * integer pixel value to scroll to
+ */
+ protected abstract void internalSetScrollPos(int px);
+
+ /**
+ * Gets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ *
+ * @return the new scroll position in pixels
+ */
+ public final double getScrollPos() {
+ assert internalGetScrollPos() == toInt32(scrollPos) : "calculated scroll position ("
+ + toInt32(scrollPos)
+ + ") did not match the DOM element scroll position ("
+ + internalGetScrollPos() + ")";
+ return scrollPos;
+ }
+
+ /**
+ * Retrieves the element's scroll position (scrollTop or scrollLeft).
+ * <p>
+ * <em>Note:</em> The parameter here is a type of integer (instead of a
+ * double) by design. The browsers internally convert all double values into
+ * an integer value. To make this fact explicit, this API has chosen to
+ * force integers already at this level.
+ *
+ * @return integer pixel value of the scroll position
+ */
+ protected abstract int internalGetScrollPos();
+
+ /**
+ * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
+ * such a way that the scrollbar is able to scroll a certain number of
+ * pixels in the axis it is representing.
+ *
+ * @param px
+ * the new size of {@link #scrollSizeElement} in the dimension
+ * this scrollbar is representing
+ */
+ protected abstract void internalSetScrollSize(double px);
+
+ /**
+ * Sets the amount of pixels the scrollbar needs to be able to scroll
+ * through.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public final void setScrollSize(final double px) {
+
+ /*
+ * This needs to be made step-by-step because IE8 flat-out refuses to
+ * fire a scroll event when the scroll size becomes smaller than the
+ * offset size. All other browser need to suffer alongside.
+ */
+
+ boolean newScrollSizeIsSmallerThanOffsetSize = px <= getOffsetSize();
+ boolean scrollSizeBecomesSmallerThanOffsetSize = showsScrollHandle()
+ && newScrollSizeIsSmallerThanOffsetSize;
+ if (scrollSizeBecomesSmallerThanOffsetSize && getScrollPos() != 0) {
+ // must be a field because Java insists.
+ scrollSizeTemporaryScrollHandler = addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ setScrollSizeNow(px);
+ scrollSizeTemporaryScrollHandler.removeHandler();
+ scrollSizeTemporaryScrollHandler = null;
+ }
+ });
+ setScrollPos(0);
+ } else {
+ setScrollSizeNow(px);
+ }
+ }
+
+ private void setScrollSizeNow(double px) {
+ internalSetScrollSize(Math.max(0, px));
+ recalculateMaxScrollPos();
+ forceScrollbar(showsScrollHandle());
+ fireVisibilityChangeIfNeeded();
+ }
+
+ /**
+ * Gets the amount of pixels the scrollbar needs to be able to scroll
+ * through.
+ *
+ * @return the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public double getScrollSize() {
+ return parseCssDimensionToPixels(internalGetScrollSize());
+ }
+
+ /**
+ * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
+ * opposite axis to what the scrollbar is representing.
+ *
+ * @param px
+ * the dimension that {@link #scrollSizeElement} should take in
+ * the opposite axis to what the scrollbar is representing
+ */
+ protected abstract void internalSetScrollbarThickness(double px);
+
+ /**
+ * Sets the scrollbar's thickness.
+ * <p>
+ * If the thickness is set to 0, the scrollbar will be treated as an
+ * "invisible" scrollbar. This means, the DOM structure will be given a
+ * non-zero size, but {@link #getScrollbarThickness()} will still return the
+ * value 0.
+ *
+ * @param px
+ * the scrollbar's thickness in pixels
+ */
+ public final void setScrollbarThickness(double px) {
+ isInvisibleScrollbar = (px == 0);
+
+ if (isInvisibleScrollbar) {
+ Event.sinkEvents(root, Event.ONSCROLL);
+ Event.setEventListener(root, new EventListener() {
+ @Override
+ public void onBrowserEvent(Event event) {
+ invisibleScrollbarTemporaryResizer.show();
+ }
+ });
+ } else {
+ Event.sinkEvents(root, 0);
+ Event.setEventListener(root, null);
+ }
+
+ internalSetScrollbarThickness(Math.max(1d, px));
+ }
+
+ /**
+ * Gets the scrollbar's thickness as defined in the DOM.
+ *
+ * @return the scrollbar's thickness as defined in the DOM, in pixels
+ */
+ protected abstract String internalGetScrollbarThickness();
+
+ /**
+ * Gets the scrollbar's thickness.
+ * <p>
+ * This value will differ from the value in the DOM, if the thickness was
+ * set to 0 with {@link #setScrollbarThickness(double)}, as the scrollbar is
+ * then treated as "invisible."
+ *
+ * @return the scrollbar's thickness in pixels
+ */
+ public final double getScrollbarThickness() {
+ if (!isInvisibleScrollbar) {
+ return parseCssDimensionToPixels(internalGetScrollbarThickness());
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Checks whether the scrollbar's handle is visible.
+ * <p>
+ * In other words, this method checks whether the contents is larger than
+ * can visually fit in the element.
+ *
+ * @return <code>true</code> iff the scrollbar's handle is visible
+ */
+ public boolean showsScrollHandle() {
+ return getOffsetSize() < getScrollSize();
+ }
+
+ public void recalculateMaxScrollPos() {
+ double scrollSize = getScrollSize();
+ double offsetSize = getOffsetSize();
+ maxScrollPos = Math.max(0, scrollSize - offsetSize);
+
+ // make sure that the correct max scroll position is maintained.
+ setScrollPos(scrollPos);
+ }
+
+ /**
+ * This is a method that JSNI can call to synchronize the object state from
+ * the DOM.
+ */
+ private final void updateScrollPosFromDom() {
+
+ /*
+ * TODO: this method probably shouldn't be called from Escalator's JSNI,
+ * but probably could be handled internally by this listening to its own
+ * element. Would clean up the code quite a bit. Needs further
+ * investigation.
+ */
+
+ int newScrollPos = internalGetScrollPos();
+ if (!isLocked()) {
+ scrollPos = newScrollPos;
+ scrollEventFirer.scheduleEvent();
+ } else if (scrollPos != newScrollPos) {
+ // we need to actually undo the setting of the scroll.
+ internalSetScrollPos(toInt32(scrollPos));
+ }
+ }
+
+ protected HandlerManager getHandlerManager() {
+ if (handlerManager == null) {
+ handlerManager = new HandlerManager(this);
+ }
+ return handlerManager;
+ }
+
+ /**
+ * Adds handler for the scrollbar handle visibility.
+ *
+ * @param handler
+ * the {@link VisibilityHandler} to add
+ * @return {@link HandlerRegistration} used to remove the handler
+ */
+ public HandlerRegistration addVisibilityHandler(
+ final VisibilityHandler handler) {
+ return getHandlerManager().addHandler(VisibilityChangeEvent.TYPE,
+ handler);
+ }
+
+ private void fireVisibilityChangeIfNeeded() {
+ final boolean oldHandleIsVisible = scrollHandleIsVisible;
+ scrollHandleIsVisible = showsScrollHandle();
+ if (oldHandleIsVisible != scrollHandleIsVisible) {
+ final VisibilityChangeEvent event = new VisibilityChangeEvent(
+ scrollHandleIsVisible);
+ getHandlerManager().fireEvent(event);
+ }
+ }
+
+ /**
+ * Converts a double into an integer by JavaScript's terms.
+ * <p>
+ * Implementation copied from {@link Element#toInt32(double)}.
+ *
+ * @param val
+ * the double value to convert into an integer
+ * @return the double value converted to an integer
+ */
+ private static native int toInt32(double val)
+ /*-{
+ return val | 0;
+ }-*/;
+
+ /**
+ * Locks or unlocks the scrollbar bundle.
+ * <p>
+ * A locked scrollbar bundle will refuse to scroll, both programmatically
+ * and via user-triggered events.
+ *
+ * @param isLocked
+ * <code>true</code> to lock, <code>false</code> to unlock
+ */
+ public void setLocked(boolean isLocked) {
+ this.isLocked = isLocked;
+ }
+
+ /**
+ * Checks whether the scrollbar bundle is locked or not.
+ *
+ * @return <code>true</code> iff the scrollbar bundle is locked
+ */
+ public boolean isLocked() {
+ return isLocked;
+ }
+
+ /**
+ * Returns the scroll direction of this scrollbar bundle.
+ *
+ * @return the scroll direction of this scrollbar bundle
+ */
+ public abstract Direction getDirection();
+
+ /**
+ * Adds a scroll handler to the scrollbar bundle.
+ *
+ * @param handler
+ * the handler to add
+ * @return the registration object for the handler registration
+ */
+ public HandlerRegistration addScrollHandler(final ScrollHandler handler) {
+ return getHandlerManager().addHandler(ScrollEvent.TYPE, handler);
+ }
+
+ private static double parseCssDimensionToPixels(String size) {
+
+ /*
+ * Sizes of elements are calculated from CSS rather than
+ * element.getOffset*() because those values are 0 whenever display:
+ * none. Because we know that all elements have populated
+ * CSS-dimensions, it's better to do it that way.
+ *
+ * Another solution would be to make the elements visible while
+ * measuring and then re-hide them, but that would cause unnecessary
+ * reflows that would probably kill the performance dead.
+ */
+
+ if (size.isEmpty()) {
+ return 0;
+ } else {
+ assert size.endsWith("px") : "Can't parse CSS dimension \"" + size
+ + "\"";
+ return Double.parseDouble(size.substring(0, size.length() - 2));
+ }
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return scrollSizeTemporaryScrollHandler != null
+ || offsetSizeTemporaryScrollHandler != null;
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/CellReference.java b/client/src/com/vaadin/client/widget/grid/CellReference.java
new file mode 100644
index 0000000000..a2e841de43
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/CellReference.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * A data class which contains information which identifies a cell in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance of
+ * this object is subject to change without the user knowing it and so should
+ * not be stored anywhere outside of the method providing these instances.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the type of the row object containing this cell
+ * @since 7.4
+ */
+public class CellReference<T> {
+ private int columnIndex;
+ private Grid.Column<?, T> column;
+ private final RowReference<T> rowReference;
+
+ public CellReference(RowReference<T> rowReference) {
+ this.rowReference = rowReference;
+ }
+
+ /**
+ * Sets the identifying information for this cell.
+ *
+ * @param columnIndex
+ * the index of the column
+ * @param column
+ * the column object
+ */
+ public void set(int columnIndex, Grid.Column<?, T> column) {
+ this.columnIndex = columnIndex;
+ this.column = column;
+ }
+
+ /**
+ * Gets the grid that contains the referenced cell.
+ *
+ * @return the grid that contains referenced cell
+ */
+ public Grid<T> getGrid() {
+ return rowReference.getGrid();
+ }
+
+ /**
+ * Gets the row index of the row.
+ *
+ * @return the index of the row
+ */
+ public int getRowIndex() {
+ return rowReference.getRowIndex();
+ }
+
+ /**
+ * Gets the row data object.
+ *
+ * @return the row object
+ */
+ public T getRow() {
+ return rowReference.getRow();
+ }
+
+ /**
+ * Gets the index of the column.
+ *
+ * @return the index of the column
+ */
+ public int getColumnIndex() {
+ return columnIndex;
+ }
+
+ /**
+ * Gets the column objects.
+ *
+ * @return the column object
+ */
+ public Grid.Column<?, T> getColumn() {
+ return column;
+ }
+
+ /**
+ * Gets the value of the cell.
+ *
+ * @return the value of the cell
+ */
+ public Object getValue() {
+ return getColumn().getValue(getRow());
+ }
+
+ /**
+ * Get the element of the cell.
+ *
+ * @return the element of the cell
+ */
+ public TableCellElement getElement() {
+ return rowReference.getElement().getCells().getItem(columnIndex);
+ }
+
+ /**
+ * Gets the RowReference for this CellReference.
+ *
+ * @return the row reference
+ */
+ protected RowReference<T> getRowReference() {
+ return rowReference;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/CellStyleGenerator.java b/client/src/com/vaadin/client/widget/grid/CellStyleGenerator.java
new file mode 100644
index 0000000000..bbc540de64
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/CellStyleGenerator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Callback interface for generating custom style names for cells
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type of the target grid
+ * @see Grid#setCellStyleGenerator(CellStyleGenerator)
+ * @since 7.4
+ */
+public interface CellStyleGenerator<T> {
+
+ /**
+ * Called by Grid to generate a style name for a column element.
+ *
+ * @param cellReference
+ * The cell to generate a style for
+ * @return the style name to add to this cell, or {@code null} to not set
+ * any style
+ */
+ public abstract String getStyle(CellReference<T> cellReference);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/DataAvailableEvent.java b/client/src/com/vaadin/client/widget/grid/DataAvailableEvent.java
new file mode 100644
index 0000000000..d88fce4e11
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/DataAvailableEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Event object describing a change of row availability in DataSource of a Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DataAvailableEvent extends GwtEvent<DataAvailableHandler> {
+
+ private Range rowsAvailable;
+ public static final Type<DataAvailableHandler> TYPE = new Type<DataAvailableHandler>();
+
+ public DataAvailableEvent(Range rowsAvailable) {
+ this.rowsAvailable = rowsAvailable;
+ }
+
+ /**
+ * Returns the range of available rows in {@link DataSource} for this event.
+ *
+ * @return range of available rows
+ */
+ public Range getAvailableRows() {
+ return rowsAvailable;
+ }
+
+ @Override
+ public Type<DataAvailableHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(DataAvailableHandler handler) {
+ handler.onDataAvailable(this);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/DataAvailableHandler.java b/client/src/com/vaadin/client/widget/grid/DataAvailableHandler.java
new file mode 100644
index 0000000000..5e0650bc41
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/DataAvailableHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link DataAvailableEvent}s.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface DataAvailableHandler extends EventHandler {
+
+ /**
+ * Called when DataSource has data available. Supplied with row range.
+ *
+ * @param availableRows
+ * Range of rows available in the DataSource
+ * @return true if the command was successfully completed, false to call
+ * again the next time new data is available
+ */
+ public void onDataAvailable(DataAvailableEvent event);
+}
diff --git a/client/src/com/vaadin/client/widget/grid/EditorHandler.java b/client/src/com/vaadin/client/widget/grid/EditorHandler.java
new file mode 100644
index 0000000000..07ec1b231c
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/EditorHandler.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * An interface for binding widgets and data to the grid row editor. Used by the
+ * editor to support different row types, data sources and custom data binding
+ * mechanisms.
+ *
+ * @param <T>
+ * the row data type
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface EditorHandler<T> {
+
+ /**
+ * A request class passed as a parameter to the editor handler methods. The
+ * request is callback-based to facilitate usage with remote or otherwise
+ * asynchronous data sources.
+ * <p>
+ * An implementation must call either {@link #success()} or {@link #fail()},
+ * according to whether the operation was a success or failed during
+ * execution, respectively.
+ *
+ * @param <T>
+ * the row data type
+ */
+ public static class EditorRequest<T> {
+
+ /**
+ * A callback interface used to notify the invoker of the editor handler
+ * of completed editor requests.
+ *
+ * @param <T>
+ * the row data type
+ */
+ public interface RequestCallback<T> {
+ /**
+ * The method that must be called when the request has been
+ * processed correctly.
+ *
+ * @param request
+ * the original request object
+ */
+ public void onSuccess(EditorRequest<T> request);
+
+ /**
+ * The method that must be called when processing the request has
+ * produced an aborting error.
+ *
+ * @param request
+ * the original request object
+ */
+ public void onError(EditorRequest<T> request);
+ }
+
+ private Grid<T> grid;
+ private int rowIndex;
+ private RequestCallback<T> callback;
+ private boolean completed = false;
+
+ /**
+ * Creates a new editor request.
+ *
+ * @param rowIndex
+ * the index of the edited row
+ * @param callback
+ * the callback invoked when the request is ready, or null if
+ * no need to call back
+ */
+ public EditorRequest(Grid<T> grid, int rowIndex,
+ RequestCallback<T> callback) {
+ this.grid = grid;
+ this.rowIndex = rowIndex;
+ this.callback = callback;
+ }
+
+ /**
+ * Returns the index of the row being requested.
+ *
+ * @return the row index
+ */
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ /**
+ * Returns the row data related to the row being requested.
+ *
+ * @return the row data
+ */
+ public T getRow() {
+ return grid.getDataSource().getRow(rowIndex);
+ }
+
+ /**
+ * Returns the grid instance related to this editor request.
+ *
+ * @return the grid instance
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Returns the editor widget used to edit the values of the given
+ * column.
+ *
+ * @param column
+ * the column whose widget to get
+ * @return the widget related to the column
+ */
+ public Widget getWidget(Grid.Column<?, T> column) {
+ Widget w = grid.getEditorWidget(column);
+ assert w != null;
+ return w;
+ }
+
+ /**
+ * Completes this request. The request can only be completed once. This
+ * method should only be called by an EditorHandler implementer if the
+ * request handling is asynchronous in nature and {@link #startAsync()}
+ * is previously invoked for this request. Synchronous requests are
+ * completed automatically by the editor.
+ *
+ * @throws IllegalStateException
+ * if the request is already completed
+ */
+ private void complete() {
+ if (completed) {
+ throw new IllegalStateException(
+ "An EditorRequest must be completed exactly once");
+ }
+ completed = true;
+ }
+
+ /**
+ * Informs Grid that the editor request was a success.
+ */
+ public void success() {
+ complete();
+ if (callback != null) {
+ callback.onSuccess(this);
+ }
+ }
+
+ /**
+ * Informs Grid that an error occurred while trying to process the
+ * request.
+ */
+ public void fail() {
+ complete();
+ if (callback != null) {
+ callback.onError(this);
+ }
+ }
+
+ /**
+ * Checks whether the request is completed or not.
+ *
+ * @return <code>true</code> iff the request is completed
+ */
+ public boolean isCompleted() {
+ return completed;
+ }
+ }
+
+ /**
+ * Binds row data to the editor widgets. Called by the editor when it is
+ * opened for editing.
+ * <p>
+ * The implementation <em>must</em> call either
+ * {@link EditorRequest#success()} or {@link EditorRequest#fail()} to signal
+ * a successful or a failed (respectively) bind action.
+ *
+ * @param request
+ * the data binding request
+ *
+ * @see Grid#editRow(int)
+ */
+ public void bind(EditorRequest<T> request);
+
+ /**
+ * Called by the editor when editing is cancelled. This method may have an
+ * empty implementation in case no special processing is required.
+ * <p>
+ * In contrast to {@link #bind(EditorRequest)} and
+ * {@link #save(EditorRequest)}, any calls to
+ * {@link EditorRequest#success()} or {@link EditorRequest#fail()} have no
+ * effect on the outcome of the cancel action. The editor is already closed
+ * when this method is called.
+ *
+ * @param request
+ * the cancel request
+ *
+ * @see Grid#cancelEditor()
+ */
+ public void cancel(EditorRequest<T> request);
+
+ /**
+ * Commits changes in the currently active edit to the data source. Called
+ * by the editor when changes are saved.
+ * <p>
+ * The implementation <em>must</em> call either
+ * {@link EditorRequest#success()} or {@link EditorRequest#fail()} to signal
+ * a successful or a failed (respectively) save action.
+ *
+ * @param request
+ * the save request
+ *
+ * @see Grid#saveEditor()
+ */
+ public void save(EditorRequest<T> request);
+
+ /**
+ * Returns a widget instance that is used to edit the values in the given
+ * column. A null return value means the column is not editable.
+ *
+ * @param column
+ * the column whose values should be edited
+ * @return the editor widget for the column or null if the column is not
+ * editable
+ */
+ public Widget getWidget(Grid.Column<?, T> column);
+}
diff --git a/client/src/com/vaadin/client/widget/grid/EventCellReference.java b/client/src/com/vaadin/client/widget/grid/EventCellReference.java
new file mode 100644
index 0000000000..cf13798e11
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/EventCellReference.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * A data class which contains information which identifies a cell being the
+ * target of an event from {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance of
+ * this object is subject to change without the user knowing it and so should
+ * not be stored anywhere outside of the method providing these instances.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class EventCellReference<T> extends CellReference<T> {
+
+ private Grid<T> grid;
+ private TableCellElement element;
+
+ public EventCellReference(Grid<T> grid) {
+ super(new RowReference<T>(grid));
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the RowReference and CellReference to point to given Cell.
+ *
+ * @param targetCell
+ * cell to point to
+ */
+ public void set(Cell targetCell) {
+ int row = targetCell.getRow();
+ int column = targetCell.getColumn();
+ // At least for now we don't need to have the actual TableRowElement
+ // available.
+ getRowReference().set(row, grid.getDataSource().getRow(row), null);
+ set(column, grid.getColumn(column));
+
+ this.element = targetCell.getElement();
+ }
+
+ @Override
+ public TableCellElement getElement() {
+ return element;
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/RendererCellReference.java b/client/src/com/vaadin/client/widget/grid/RendererCellReference.java
new file mode 100644
index 0000000000..533eafded6
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/RendererCellReference.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * A data class which contains information which identifies a cell being
+ * rendered in a {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance of
+ * this object is subject to change without the user knowing it and so should
+ * not be stored anywhere outside of the method providing these instances.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RendererCellReference extends CellReference<Object> {
+
+ /**
+ * Creates a new renderer cell reference bound to a row reference.
+ *
+ * @param rowReference
+ * the row reference to bind to
+ */
+ public RendererCellReference(RowReference<Object> rowReference) {
+ super(rowReference);
+ }
+
+ private FlyweightCell cell;
+
+ /**
+ * Sets the identifying information for this cell.
+ *
+ * @param cell
+ * the flyweight cell to reference
+ * @param column
+ * the column to reference
+ */
+ public void set(FlyweightCell cell, Grid.Column<?, ?> column) {
+ this.cell = cell;
+ super.set(cell.getColumn(), (Grid.Column<?, Object>) column);
+ }
+
+ /**
+ * Returns the element of the cell. Can be either a <code>TD</code> element
+ * or a <code>TH</code> element.
+ *
+ * @return the element of the cell
+ */
+ @Override
+ public TableCellElement getElement() {
+ return cell.getElement();
+ }
+
+ /**
+ * Sets the colspan attribute of the element of this cell.
+ *
+ * @param numberOfCells
+ * the number of columns that the cell should span
+ */
+ public void setColSpan(int numberOfCells) {
+ cell.setColSpan(numberOfCells);
+ }
+
+ /**
+ * Gets the colspan attribute of the element of this cell.
+ *
+ * @return the number of columns that the cell should span
+ */
+ public int getColSpan() {
+ return cell.getColSpan();
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/RowReference.java b/client/src/com/vaadin/client/widget/grid/RowReference.java
new file mode 100644
index 0000000000..8874fcc5cc
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/RowReference.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * A data class which contains information which identifies a row in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance of
+ * this object is subject to change without the user knowing it and so should
+ * not be stored anywhere outside of the method providing these instances.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row object type
+ * @since 7.4
+ */
+public class RowReference<T> {
+ private final Grid<T> grid;
+
+ private int rowIndex;
+ private T row;
+
+ private TableRowElement element;
+
+ /**
+ * Creates a new row reference for the given grid.
+ *
+ * @param grid
+ * the grid that the row belongs to
+ */
+ public RowReference(Grid<T> grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the identifying information for this row.
+ *
+ * @param rowIndex
+ * the index of the row
+ * @param row
+ * the row object
+ * @param elemenet
+ * the element of the row
+ */
+ public void set(int rowIndex, T row, TableRowElement element) {
+ this.rowIndex = rowIndex;
+ this.row = row;
+ this.element = element;
+ }
+
+ /**
+ * Gets the grid that contains the referenced row.
+ *
+ * @return the grid that contains referenced row
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the row index of the row.
+ *
+ * @return the index of the row
+ */
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ /**
+ * Gets the row data object.
+ *
+ * @return the row object
+ */
+ public T getRow() {
+ return row;
+ }
+
+ /**
+ * Gets the table row element of the row.
+ *
+ * @return the element of the row
+ */
+ public TableRowElement getElement() {
+ return element;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/RowStyleGenerator.java b/client/src/com/vaadin/client/widget/grid/RowStyleGenerator.java
new file mode 100644
index 0000000000..a12a9ff47d
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/RowStyleGenerator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid;
+
+import java.io.Serializable;
+
+/**
+ * Callback interface for generating custom style names for data rows
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type of the target grid
+ * @see Grid#setRowStyleGenerator(RowStyleGenerator)
+ * @since 7.4
+ */
+public interface RowStyleGenerator<T> extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a row.
+ *
+ * @param rowReference
+ * The row to generate a style for
+ * @return the style name to add to this row, or {@code null} to not set any
+ * style
+ */
+ public abstract String getStyle(RowReference<T> rowReference);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/datasources/ListDataSource.java b/client/src/com/vaadin/client/widget/grid/datasources/ListDataSource.java
new file mode 100644
index 0000000000..56e1db5c36
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/datasources/ListDataSource.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.datasources;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.client.widget.grid.events.SelectAllHandler;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * A simple list based on an in-memory data source for simply adding a list of
+ * row pojos to the grid. Based on a wrapped list instance which supports adding
+ * and removing of items.
+ *
+ * <p>
+ * Usage:
+ *
+ * <pre>
+ * ListDataSource&lt;Integer&gt; ds = new ListDataSource&lt;Integer&gt;(1, 2, 3, 4);
+ *
+ * // Add item to the data source
+ * ds.asList().add(5);
+ *
+ * // Remove item from the data source
+ * ds.asList().remove(3);
+ *
+ * // Add multiple items
+ * ds.asList().addAll(Arrays.asList(5, 6, 7));
+ * </pre>
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ListDataSource<T> implements DataSource<T> {
+
+ private class RowHandleImpl extends RowHandle<T> {
+
+ private final T row;
+
+ public RowHandleImpl(T row) {
+ this.row = row;
+ }
+
+ @Override
+ public T getRow() {
+ /*
+ * We'll cheat here and don't throw an IllegalStateException even if
+ * this isn't pinned, because we know that the reference never gets
+ * stale.
+ */
+ return row;
+ }
+
+ @Override
+ public void pin() {
+ // NOOP, really
+ }
+
+ @Override
+ public void unpin() throws IllegalStateException {
+ /*
+ * Just to make things easier for everyone, we won't throw the
+ * exception, even in illegal situations.
+ */
+ }
+
+ @Override
+ protected boolean equalsExplicit(Object obj) {
+ if (obj instanceof ListDataSource.RowHandleImpl) {
+ /*
+ * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I
+ * like the @SuppressWarnings more (keeps the line length in
+ * check.)
+ */
+ @SuppressWarnings("unchecked")
+ RowHandleImpl rhi = (RowHandleImpl) obj;
+ return SharedUtil.equals(row, rhi.row);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected int hashCodeExplicit() {
+ return row.hashCode();
+ }
+
+ @Override
+ public void updateRow() {
+ changeHandler.dataUpdated(ds.indexOf(getRow()), 1);
+ }
+ }
+
+ /**
+ * Wraps the datasource list and notifies the change handler of changing to
+ * the list
+ */
+ private class ListWrapper implements List<T> {
+
+ @Override
+ public int size() {
+ return ds.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return ds.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return contains(o);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return new ListWrapperIterator(ds.iterator());
+ }
+
+ @Override
+ public Object[] toArray() {
+ return ds.toArray();
+ }
+
+ @Override
+ @SuppressWarnings("hiding")
+ public <T> T[] toArray(T[] a) {
+ return toArray(a);
+ }
+
+ @Override
+ public boolean add(T e) {
+ if (ds.add(e)) {
+ if (changeHandler != null) {
+ changeHandler.dataAdded(ds.size() - 1, 1);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ int index = ds.indexOf(o);
+ if (ds.remove(o)) {
+ if (changeHandler != null) {
+ changeHandler.dataRemoved(index, 1);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return ds.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends T> c) {
+ int idx = ds.size();
+ if (ds.addAll(c)) {
+ if (changeHandler != null) {
+ changeHandler.dataAdded(idx, c.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends T> c) {
+ if (ds.addAll(index, c)) {
+ if (changeHandler != null) {
+ changeHandler.dataAdded(index, c.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ if (ds.removeAll(c)) {
+ if (changeHandler != null) {
+ // Have to update the whole list as the removal does not
+ // have to be a continuous range
+ changeHandler.dataUpdated(0, ds.size());
+ changeHandler.dataAvailable(0, ds.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ if (ds.retainAll(c)) {
+ if (changeHandler != null) {
+ // Have to update the whole list as the retain does not
+ // have to be a continuous range
+ changeHandler.dataUpdated(0, ds.size());
+ changeHandler.dataAvailable(0, ds.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ int size = ds.size();
+ ds.clear();
+ if (changeHandler != null) {
+ changeHandler.dataRemoved(0, size);
+ }
+ }
+
+ @Override
+ public T get(int index) {
+ return ds.get(index);
+ }
+
+ @Override
+ public T set(int index, T element) {
+ T prev = ds.set(index, element);
+ if (changeHandler != null) {
+ changeHandler.dataUpdated(index, 1);
+ }
+ return prev;
+ }
+
+ @Override
+ public void add(int index, T element) {
+ ds.add(index, element);
+ if (changeHandler != null) {
+ changeHandler.dataAdded(index, 1);
+ }
+ }
+
+ @Override
+ public T remove(int index) {
+ T removed = ds.remove(index);
+ if (changeHandler != null) {
+ changeHandler.dataRemoved(index, 1);
+ }
+ return removed;
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ return ds.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return ds.lastIndexOf(o);
+ }
+
+ @Override
+ public ListIterator<T> listIterator() {
+ // TODO could be implemented by a custom iterator.
+ throw new UnsupportedOperationException(
+ "List iterators not supported at this time.");
+ }
+
+ @Override
+ public ListIterator<T> listIterator(int index) {
+ // TODO could be implemented by a custom iterator.
+ throw new UnsupportedOperationException(
+ "List iterators not supported at this time.");
+ }
+
+ @Override
+ public List<T> subList(int fromIndex, int toIndex) {
+ throw new UnsupportedOperationException("Sub lists not supported.");
+ }
+ }
+
+ /**
+ * Iterator returned by {@link ListWrapper}
+ */
+ private class ListWrapperIterator implements Iterator<T> {
+
+ private final Iterator<T> iterator;
+
+ /**
+ * Constructs a new iterator
+ */
+ public ListWrapperIterator(Iterator<T> iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public T next() {
+ return iterator.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Iterator.remove() is not supported by this iterator.");
+ }
+ }
+
+ /**
+ * Datasource for providing row pojo's
+ */
+ private final List<T> ds;
+
+ /**
+ * Wrapper that wraps the data source
+ */
+ private final ListWrapper wrapper;
+
+ /**
+ * Handler for listening to changes in the underlying list.
+ */
+ private DataChangeHandler changeHandler;
+
+ /**
+ * Constructs a new list data source.
+ * <p>
+ * Note: Modifications to the original list will not be reflected in the
+ * data source after the data source has been constructed. To add or remove
+ * items to the data source after it has been constructed use
+ * {@link ListDataSource#asList()}.
+ *
+ *
+ * @param datasource
+ * The list to use for providing the data to the grid
+ */
+ public ListDataSource(List<T> datasource) {
+ if (datasource == null) {
+ throw new IllegalArgumentException("datasource cannot be null");
+ }
+ ds = new ArrayList<T>(datasource);
+ wrapper = new ListWrapper();
+ }
+
+ /**
+ * Constructs a data source with a set of rows. You can dynamically add and
+ * remove rows from the data source via the list you get from
+ * {@link ListDataSource#asList()}
+ *
+ * @param rows
+ * The rows to initially add to the data source
+ */
+ public ListDataSource(T... rows) {
+ if (rows == null) {
+ ds = new ArrayList<T>();
+ } else {
+ ds = new ArrayList<T>(Arrays.asList(rows));
+ }
+ wrapper = new ListWrapper();
+ }
+
+ @Override
+ public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+ if (firstRowIndex >= ds.size()) {
+ throw new IllegalStateException(
+ "Trying to fetch rows outside of array");
+ }
+ changeHandler.dataAvailable(firstRowIndex, numberOfRows);
+ }
+
+ @Override
+ public T getRow(int rowIndex) {
+ return ds.get(rowIndex);
+ }
+
+ @Override
+ public int size() {
+ return ds.size();
+ }
+
+ @Override
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+ this.changeHandler = dataChangeHandler;
+ }
+
+ /**
+ * Gets the list that backs this datasource. Any changes made to this list
+ * will be reflected in the datasource.
+ * <p>
+ * Note: The list is not the same list as passed into the data source via
+ * the constructor.
+ *
+ * @return Returns a list implementation that wraps the real list that backs
+ * the data source and provides events for the data source
+ * listeners.
+ */
+ public List<T> asList() {
+ return wrapper;
+ }
+
+ @Override
+ public RowHandle<T> getHandle(T row) throws IllegalStateException {
+ assert ds.contains(row) : "This data source doesn't contain the row "
+ + row;
+ return new RowHandleImpl(row);
+ }
+
+ /**
+ * Sort entire container according to a {@link Comparator}.
+ *
+ * @param comparator
+ * a comparator object, which compares two data source entries
+ * (beans/pojos)
+ */
+ public void sort(Comparator<T> comparator) {
+ Collections.sort(ds, comparator);
+ if (changeHandler != null) {
+ changeHandler.dataUpdated(0, ds.size());
+ }
+ }
+
+ @Override
+ public int indexOf(T row) {
+ return ds.indexOf(row);
+ }
+
+ /**
+ * Returns a {@link SelectAllHandler} for this ListDataSource.
+ *
+ * @return select all handler
+ */
+ public SelectAllHandler<T> getSelectAllHandler() {
+ return new SelectAllHandler<T>() {
+ @Override
+ public void onSelectAll(SelectAllEvent<T> event) {
+ event.getSelectionModel().select(asList());
+ }
+ };
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/datasources/ListSorter.java b/client/src/com/vaadin/client/widget/grid/datasources/ListSorter.java
new file mode 100644
index 0000000000..69bea629b0
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/datasources/ListSorter.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.datasources;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.widget.grid.sort.SortEvent;
+import com.vaadin.client.widget.grid.sort.SortHandler;
+import com.vaadin.client.widget.grid.sort.SortOrder;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Provides sorting facility from Grid for the {@link ListDataSource} in-memory
+ * data source.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * Grid row data type
+ * @since 7.4
+ */
+public class ListSorter<T> {
+
+ private Grid<T> grid;
+ private Map<Grid.Column<?, T>, Comparator<?>> comparators;
+ private HandlerRegistration sortHandlerRegistration;
+
+ public ListSorter(Grid<T> grid) {
+
+ if (grid == null) {
+ throw new IllegalArgumentException("Grid can not be null");
+ }
+
+ this.grid = grid;
+ comparators = new HashMap<Grid.Column<?, T>, Comparator<?>>();
+
+ sortHandlerRegistration = grid.addSortHandler(new SortHandler<T>() {
+ @Override
+ public void sort(SortEvent<T> event) {
+ ListSorter.this.sort(event.getOrder());
+ }
+ });
+ }
+
+ /**
+ * Detach this Sorter from the Grid. This unregisters the sort event handler
+ * which was used to apply sorting to the ListDataSource.
+ */
+ public void removeFromGrid() {
+ sortHandlerRegistration.removeHandler();
+ }
+
+ /**
+ * Assign or remove a comparator for a column. This comparator method, if
+ * defined, is always used in favour of 'natural' comparison of objects
+ * (i.e. the compareTo of objects implementing the Comparable interface,
+ * which includes all standard data classes like String, Number derivatives
+ * and Dates). Any existing comparator can be removed by passing in a
+ * non-null GridColumn and a null Comparator.
+ *
+ * @param column
+ * a grid column. May not be null.
+ * @param comparator
+ * comparator method for the values returned by the grid column.
+ * If null, any existing comparator is removed.
+ */
+ public <C> void setComparator(Grid.Column<C, T> column,
+ Comparator<C> comparator) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Column reference can not be null");
+ }
+ if (comparator == null) {
+ comparators.remove(column);
+ } else {
+ comparators.put(column, comparator);
+ }
+ }
+
+ /**
+ * Retrieve the comparator assigned for a specific grid column.
+ *
+ * @param column
+ * a grid column. May not be null.
+ * @return a comparator, or null if no comparator for the specified grid
+ * column has been set.
+ */
+ @SuppressWarnings("unchecked")
+ public <C> Comparator<C> getComparator(Grid.Column<C, T> column) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Column reference can not be null");
+ }
+ return (Comparator<C>) comparators.get(column);
+ }
+
+ /**
+ * Remove all comparator mappings. Useful if the data source has changed but
+ * this Sorter is being re-used.
+ */
+ public void clearComparators() {
+ comparators.clear();
+ }
+
+ /**
+ * Apply sorting to the current ListDataSource.
+ *
+ * @param order
+ * the sort order list provided by the grid sort event
+ */
+ private void sort(final List<SortOrder> order) {
+ DataSource<T> ds = grid.getDataSource();
+ if (!(ds instanceof ListDataSource)) {
+ throw new IllegalStateException("Grid " + grid
+ + " data source is not a ListDataSource!");
+ }
+
+ ((ListDataSource<T>) ds).sort(new Comparator<T>() {
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public int compare(T a, T b) {
+
+ for (SortOrder o : order) {
+
+ Grid.Column column = o.getColumn();
+ Comparator cmp = ListSorter.this.comparators.get(column);
+ int result = 0;
+ Object value_a = column.getValue(a);
+ Object value_b = column.getValue(b);
+ if (cmp != null) {
+ result = cmp.compare(value_a, value_b);
+ } else {
+ if (!(value_a instanceof Comparable)) {
+ throw new IllegalStateException("Column " + column
+ + " has no assigned comparator and value "
+ + value_a + " isn't naturally comparable");
+ }
+ result = ((Comparable) value_a).compareTo(value_b);
+ }
+
+ if (result != 0) {
+ return o.getDirection() == SortDirection.ASCENDING ? result
+ : -result;
+ }
+ }
+
+ if (order.size() > 0) {
+ return order.get(0).getDirection() == SortDirection.ASCENDING ? a
+ .hashCode() - b.hashCode()
+ : b.hashCode() - a.hashCode();
+ }
+ return a.hashCode() - b.hashCode();
+ }
+ });
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/AbstractGridKeyEventHandler.java b/client/src/com/vaadin/client/widget/grid/events/AbstractGridKeyEventHandler.java
new file mode 100644
index 0000000000..120c32d380
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/AbstractGridKeyEventHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.vaadin.client.widgets.Grid.AbstractGridKeyEvent;
+
+/**
+ * Base interface of all handlers for {@link AbstractGridKeyEvent}s.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract interface AbstractGridKeyEventHandler extends EventHandler {
+
+ public abstract interface GridKeyDownHandler extends
+ AbstractGridKeyEventHandler {
+ public void onKeyDown(GridKeyDownEvent event);
+ }
+
+ public abstract interface GridKeyUpHandler extends
+ AbstractGridKeyEventHandler {
+ public void onKeyUp(GridKeyUpEvent event);
+ }
+
+ public abstract interface GridKeyPressHandler extends
+ AbstractGridKeyEventHandler {
+ public void onKeyPress(GridKeyPressEvent event);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/AbstractGridMouseEventHandler.java b/client/src/com/vaadin/client/widget/grid/events/AbstractGridMouseEventHandler.java
new file mode 100644
index 0000000000..15e22a6d57
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/AbstractGridMouseEventHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+import com.vaadin.client.widgets.Grid.AbstractGridMouseEvent;
+
+/**
+ * Base interface of all handlers for {@link AbstractGridMouseEvent}s.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract interface AbstractGridMouseEventHandler extends EventHandler {
+
+ public abstract interface GridClickHandler extends
+ AbstractGridMouseEventHandler {
+ public void onClick(GridClickEvent event);
+ }
+
+ public abstract interface GridDoubleClickHandler extends
+ AbstractGridMouseEventHandler {
+ public void onDoubleClick(GridDoubleClickEvent event);
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/BodyClickHandler.java b/client/src/com/vaadin/client/widget/grid/events/BodyClickHandler.java
new file mode 100644
index 0000000000..a66e170524
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/BodyClickHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+
+/**
+ * Handler for {@link GridClickEvent}s that happen in the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyClickHandler extends GridClickHandler {
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/BodyDoubleClickHandler.java b/client/src/com/vaadin/client/widget/grid/events/BodyDoubleClickHandler.java
new file mode 100644
index 0000000000..a7be5bad24
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/BodyDoubleClickHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+
+/**
+ * Handler for {@link GridDoubleClickEvent}s that happen in the body of the
+ * Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface BodyDoubleClickHandler extends GridDoubleClickHandler {
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/BodyKeyDownHandler.java b/client/src/com/vaadin/client/widget/grid/events/BodyKeyDownHandler.java
new file mode 100644
index 0000000000..ff1ae82d2e
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/BodyKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when the focused cell is in
+ * the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyDownHandler extends GridKeyDownHandler {
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/BodyKeyPressHandler.java b/client/src/com/vaadin/client/widget/grid/events/BodyKeyPressHandler.java
new file mode 100644
index 0000000000..245250d4c0
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/BodyKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when the focused cell is
+ * in the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyPressHandler extends GridKeyPressHandler {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/BodyKeyUpHandler.java b/client/src/com/vaadin/client/widget/grid/events/BodyKeyUpHandler.java
new file mode 100644
index 0000000000..2c0951ea40
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/BodyKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when the focused cell is in
+ * the body of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyUpHandler extends GridKeyUpHandler {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/FooterClickHandler.java b/client/src/com/vaadin/client/widget/grid/events/FooterClickHandler.java
new file mode 100644
index 0000000000..51fa38c948
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/FooterClickHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+
+/**
+ * Handler for {@link GridClickEvent}s that happen in the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterClickHandler extends GridClickHandler {
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/FooterDoubleClickHandler.java b/client/src/com/vaadin/client/widget/grid/events/FooterDoubleClickHandler.java
new file mode 100644
index 0000000000..3bb9c9ee72
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/FooterDoubleClickHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+
+/**
+ * Handler for {@link GridDoubleClickEvent}s that happen in the footer of the
+ * Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface FooterDoubleClickHandler extends GridDoubleClickHandler {
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/FooterKeyDownHandler.java b/client/src/com/vaadin/client/widget/grid/events/FooterKeyDownHandler.java
new file mode 100644
index 0000000000..85f83970f2
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/FooterKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when the focused cell is in
+ * the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyDownHandler extends GridKeyDownHandler {
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/FooterKeyPressHandler.java b/client/src/com/vaadin/client/widget/grid/events/FooterKeyPressHandler.java
new file mode 100644
index 0000000000..09778f6873
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/FooterKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when the focused cell is
+ * in the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyPressHandler extends GridKeyPressHandler {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/FooterKeyUpHandler.java b/client/src/com/vaadin/client/widget/grid/events/FooterKeyUpHandler.java
new file mode 100644
index 0000000000..688f89880f
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/FooterKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when the focused cell is in
+ * the footer of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyUpHandler extends GridKeyUpHandler {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/GridClickEvent.java b/client/src/com/vaadin/client/widget/grid/events/GridClickEvent.java
new file mode 100644
index 0000000000..ade878abc6
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/GridClickEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.AbstractGridMouseEvent;
+import com.vaadin.client.widgets.Grid.Section;
+
+/**
+ * Represents native mouse click event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridClickEvent extends AbstractGridMouseEvent<GridClickHandler> {
+
+ public GridClickEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.CLICK;
+ }
+
+ @Override
+ protected void doDispatch(GridClickHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyClickHandler)
+ || (section == Section.HEADER && handler instanceof HeaderClickHandler)
+ || (section == Section.FOOTER && handler instanceof FooterClickHandler)) {
+ handler.onClick(this);
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/GridDoubleClickEvent.java b/client/src/com/vaadin/client/widget/grid/events/GridDoubleClickEvent.java
new file mode 100644
index 0000000000..20e432aa85
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/GridDoubleClickEvent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.AbstractGridMouseEvent;
+import com.vaadin.client.widgets.Grid.Section;
+
+/**
+ * Represents native mouse double click event in Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridDoubleClickEvent extends
+ AbstractGridMouseEvent<GridDoubleClickHandler> {
+
+ public GridDoubleClickEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.DBLCLICK;
+ }
+
+ @Override
+ protected void doDispatch(GridDoubleClickHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyDoubleClickHandler)
+ || (section == Section.HEADER && handler instanceof HeaderDoubleClickHandler)
+ || (section == Section.FOOTER && handler instanceof FooterDoubleClickHandler)) {
+ handler.onDoubleClick(this);
+ }
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/GridKeyDownEvent.java b/client/src/com/vaadin/client/widget/grid/events/GridKeyDownEvent.java
new file mode 100644
index 0000000000..2ca7448849
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/GridKeyDownEvent.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.widgets.Grid.Section;
+
+/**
+ * Represents native key down event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridKeyDownEvent extends AbstractGridKeyEvent<GridKeyDownHandler> {
+
+ public GridKeyDownEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected void doDispatch(GridKeyDownHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyKeyDownHandler)
+ || (section == Section.HEADER && handler instanceof HeaderKeyDownHandler)
+ || (section == Section.FOOTER && handler instanceof FooterKeyDownHandler)) {
+ handler.onKeyDown(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYDOWN;
+ }
+
+ /**
+ * Does the key code represent an arrow key?
+ *
+ * @param keyCode
+ * the key code
+ * @return if it is an arrow key code
+ */
+ public static boolean isArrow(int keyCode) {
+ switch (keyCode) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_RIGHT:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_LEFT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the native key code. These key codes are enumerated in the
+ * {@link KeyCodes} class.
+ *
+ * @return the key code
+ */
+ public int getNativeKeyCode() {
+ return getNativeEvent().getKeyCode();
+ }
+
+ /**
+ * Is this a key down arrow?
+ *
+ * @return whether this is a down arrow key event
+ */
+ public boolean isDownArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Is this a left arrow?
+ *
+ * @return whether this is a left arrow key event
+ */
+ public boolean isLeftArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Is this a right arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isRightArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Is this a up arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isUpArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_UP;
+ }
+
+ @Override
+ public String toDebugString() {
+ return super.toDebugString() + "[" + getNativeKeyCode() + "]";
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/GridKeyPressEvent.java b/client/src/com/vaadin/client/widget/grid/events/GridKeyPressEvent.java
new file mode 100644
index 0000000000..7171814262
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/GridKeyPressEvent.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.widgets.Grid.Section;
+
+/**
+ * Represents native key press event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridKeyPressEvent extends
+ AbstractGridKeyEvent<GridKeyPressHandler> {
+
+ public GridKeyPressEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected void doDispatch(GridKeyPressHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyKeyPressHandler)
+ || (section == Section.HEADER && handler instanceof HeaderKeyPressHandler)
+ || (section == Section.FOOTER && handler instanceof FooterKeyPressHandler)) {
+ handler.onKeyPress(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYPRESS;
+ }
+
+ /**
+ * Gets the char code for this event.
+ *
+ * @return the char code
+ */
+ public char getCharCode() {
+ return (char) getUnicodeCharCode();
+ }
+
+ /**
+ * Gets the Unicode char code (code point) for this event.
+ *
+ * @return the Unicode char code
+ */
+ public int getUnicodeCharCode() {
+ return getNativeEvent().getCharCode();
+ }
+
+ @Override
+ public String toDebugString() {
+ return super.toDebugString() + "[" + getCharCode() + "]";
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/GridKeyUpEvent.java b/client/src/com/vaadin/client/widget/grid/events/GridKeyUpEvent.java
new file mode 100644
index 0000000000..2b761a7039
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/GridKeyUpEvent.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.widgets.Grid.Section;
+
+/**
+ * Represents native key up event in Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridKeyUpEvent extends AbstractGridKeyEvent<GridKeyUpHandler> {
+
+ public GridKeyUpEvent(Grid<?> grid, CellReference<?> targetCell) {
+ super(grid, targetCell);
+ }
+
+ @Override
+ protected void doDispatch(GridKeyUpHandler handler, Section section) {
+ if ((section == Section.BODY && handler instanceof BodyKeyUpHandler)
+ || (section == Section.HEADER && handler instanceof HeaderKeyUpHandler)
+ || (section == Section.FOOTER && handler instanceof FooterKeyUpHandler)) {
+ handler.onKeyUp(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYUP;
+ }
+
+ /**
+ * Does the key code represent an arrow key?
+ *
+ * @param keyCode
+ * the key code
+ * @return if it is an arrow key code
+ */
+ public static boolean isArrow(int keyCode) {
+ switch (keyCode) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_RIGHT:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_LEFT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the native key code. These key codes are enumerated in the
+ * {@link KeyCodes} class.
+ *
+ * @return the key code
+ */
+ public int getNativeKeyCode() {
+ return getNativeEvent().getKeyCode();
+ }
+
+ /**
+ * Is this a key down arrow?
+ *
+ * @return whether this is a down arrow key event
+ */
+ public boolean isDownArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Is this a left arrow?
+ *
+ * @return whether this is a left arrow key event
+ */
+ public boolean isLeftArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Is this a right arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isRightArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Is this a up arrow?
+ *
+ * @return whether this is a right arrow key event
+ */
+ public boolean isUpArrow() {
+ return getNativeKeyCode() == KeyCodes.KEY_UP;
+ }
+
+ @Override
+ public String toDebugString() {
+ return super.toDebugString() + "[" + getNativeKeyCode() + "]";
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/HeaderClickHandler.java b/client/src/com/vaadin/client/widget/grid/events/HeaderClickHandler.java
new file mode 100644
index 0000000000..da20e80905
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/HeaderClickHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridClickHandler;
+
+/**
+ * Handler for {@link GridClickEvent}s that happen in the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderClickHandler extends GridClickHandler {
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/HeaderDoubleClickHandler.java b/client/src/com/vaadin/client/widget/grid/events/HeaderDoubleClickHandler.java
new file mode 100644
index 0000000000..7ebb0c17f8
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/HeaderDoubleClickHandler.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler.GridDoubleClickHandler;
+
+/**
+ * Handler for {@link GridDoubleClickEvent}s that happen in the header of the
+ * Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface HeaderDoubleClickHandler extends GridDoubleClickHandler {
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/HeaderKeyDownHandler.java b/client/src/com/vaadin/client/widget/grid/events/HeaderKeyDownHandler.java
new file mode 100644
index 0000000000..555eb936af
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/HeaderKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when the focused cell is in
+ * the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyDownHandler extends GridKeyDownHandler {
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/HeaderKeyPressHandler.java b/client/src/com/vaadin/client/widget/grid/events/HeaderKeyPressHandler.java
new file mode 100644
index 0000000000..c4dd312f93
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/HeaderKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when the focused cell is
+ * in the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyPressHandler extends GridKeyPressHandler {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/HeaderKeyUpHandler.java b/client/src/com/vaadin/client/widget/grid/events/HeaderKeyUpHandler.java
new file mode 100644
index 0000000000..4dbe1c681e
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/HeaderKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when the focused cell is in
+ * the header of the Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyUpHandler extends GridKeyUpHandler {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/events/ScrollEvent.java b/client/src/com/vaadin/client/widget/grid/events/ScrollEvent.java
new file mode 100644
index 0000000000..08e1e07eab
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/ScrollEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * An event that signifies that a scrollbar bundle has been scrolled
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class ScrollEvent extends GwtEvent<ScrollHandler> {
+
+ /** The type of this event */
+ public static final Type<ScrollHandler> TYPE = new Type<ScrollHandler>();
+
+ @Override
+ public Type<ScrollHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(final ScrollHandler handler) {
+ handler.onScroll(this);
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/ScrollHandler.java b/client/src/com/vaadin/client/widget/grid/events/ScrollHandler.java
new file mode 100644
index 0000000000..1ce901e707
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/ScrollHandler.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * A handler that gets called whenever a scrollbar bundle is scrolled
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public interface ScrollHandler extends EventHandler {
+ /**
+ * A callback method that is called once a scrollbar bundle has been
+ * scrolled.
+ *
+ * @param event
+ * the scroll event
+ */
+ public void onScroll(ScrollEvent event);
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/SelectAllEvent.java b/client/src/com/vaadin/client/widget/grid/events/SelectAllEvent.java
new file mode 100644
index 0000000000..43c2055e95
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/SelectAllEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.client.widget.grid.selection.SelectionModel;
+
+/**
+ * A select all event, fired by the Grid when it needs all rows in data source
+ * to be selected.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SelectAllEvent<T> extends GwtEvent<SelectAllHandler<T>> {
+
+ /**
+ * Handler type.
+ */
+ private final static Type<SelectAllHandler<?>> TYPE = new Type<SelectAllHandler<?>>();;
+
+ private SelectionModel.Multi<T> selectionModel;
+
+ public SelectAllEvent(SelectionModel.Multi<T> selectionModel) {
+ this.selectionModel = selectionModel;
+ }
+
+ public static final Type<SelectAllHandler<?>> getType() {
+ return TYPE;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public Type<SelectAllHandler<T>> getAssociatedType() {
+ return (Type) TYPE;
+ }
+
+ @Override
+ protected void dispatch(SelectAllHandler<T> handler) {
+ handler.onSelectAll(this);
+ }
+
+ public SelectionModel.Multi<T> getSelectionModel() {
+ return selectionModel;
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/events/SelectAllHandler.java b/client/src/com/vaadin/client/widget/grid/events/SelectAllHandler.java
new file mode 100644
index 0000000000..2cdee8d1b3
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/events/SelectAllHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.events;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid select all event, called when the Grid needs all rows in
+ * data source to be selected.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface SelectAllHandler<T> extends EventHandler {
+
+ /**
+ * Called when select all value in SelectionColumn header changes value.
+ *
+ * @param event
+ * select all event telling that all rows should be selected
+ */
+ public void onSelectAll(SelectAllEvent<T> event);
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/AbstractRowHandleSelectionModel.java b/client/src/com/vaadin/client/widget/grid/selection/AbstractRowHandleSelectionModel.java
new file mode 100644
index 0000000000..6b7bbb6294
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/AbstractRowHandleSelectionModel.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+
+/**
+ * An abstract class that adds a consistent API for common methods that's needed
+ * by Vaadin's server-based selection models to work.
+ * <p>
+ * <em>Note:</em> This should be an interface instead of an abstract class, if
+ * only we could define protected methods in an interface.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * The grid's row type
+ * @since 7.4
+ */
+public abstract class AbstractRowHandleSelectionModel<T> implements
+ SelectionModel<T> {
+ /**
+ * Select a row, based on its
+ * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}.
+ * <p>
+ * <em>Note:</em> this method may not fire selection change events.
+ *
+ * @param handle
+ * the handle to select by
+ * @return <code>true</code> iff the selection state was changed by this
+ * call
+ * @throws UnsupportedOperationException
+ * if the selection model does not support either handles or
+ * selection
+ */
+ protected abstract boolean selectByHandle(RowHandle<T> handle);
+
+ /**
+ * Deselect a row, based on its
+ * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}.
+ * <p>
+ * <em>Note:</em> this method may not fire selection change events.
+ *
+ * @param handle
+ * the handle to deselect by
+ * @return <code>true</code> iff the selection state was changed by this
+ * call
+ * @throws UnsupportedOperationException
+ * if the selection model does not support either handles or
+ * deselection
+ */
+ protected abstract boolean deselectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException;
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/ClickSelectHandler.java b/client/src/com/vaadin/client/widget/grid/selection/ClickSelectHandler.java
new file mode 100644
index 0000000000..0a1154e787
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/ClickSelectHandler.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.client.widget.grid.events.GridClickEvent;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Generic class to perform selections when clicking on cells in body of Grid.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ClickSelectHandler<T> {
+
+ private Grid<T> grid;
+ private HandlerRegistration clickHandler;
+
+ private class RowClickHandler implements BodyClickHandler {
+
+ @Override
+ public void onClick(GridClickEvent event) {
+ T row = (T) event.getTargetCell().getRow();
+ if (!grid.isSelected(row)) {
+ grid.select(row);
+ }
+ }
+ }
+
+ /**
+ * Constructor for ClickSelectHandler. This constructor will add all
+ * necessary handlers for selecting rows by clicking cells.
+ *
+ * @param grid
+ * grid to attach to
+ */
+ public ClickSelectHandler(Grid<T> grid) {
+ this.grid = grid;
+ clickHandler = grid.addBodyClickHandler(new RowClickHandler());
+ }
+
+ /**
+ * Clean up function for removing all now obsolete handlers.
+ */
+ public void removeHandler() {
+ clickHandler.removeHandler();
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/HasSelectionHandlers.java b/client/src/com/vaadin/client/widget/grid/selection/HasSelectionHandlers.java
new file mode 100644
index 0000000000..ffcad4c903
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/HasSelectionHandlers.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+
+/**
+ * Marker interface for widgets that fires selection events.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public interface HasSelectionHandlers<T> {
+
+ /**
+ * Register a selection change handler.
+ * <p>
+ * This handler is called whenever a
+ * {@link com.vaadin.ui.components.grid.selection.SelectionModel
+ * SelectionModel} detects a change in selection state.
+ *
+ * @param handler
+ * a {@link SelectionHandler}
+ * @return a handler registration object, which can be used to remove the
+ * handler.
+ */
+ public HandlerRegistration addSelectionHandler(SelectionHandler<T> handler);
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java b/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java
new file mode 100644
index 0000000000..5024c8bffa
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java
@@ -0,0 +1,719 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import com.google.gwt.animation.client.AnimationScheduler;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.InputElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.dom.client.TableSectionElement;
+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.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.grid.selection.SelectionModel.Multi.Batched;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Renderer showing multi selection check boxes.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * the type of the associated grid
+ * @since 7.4
+ */
+public class MultiSelectionRenderer<T> extends ComplexRenderer<Boolean> {
+
+ /** The size of the autoscroll area, both top and bottom. */
+ private static final int SCROLL_AREA_GRADIENT_PX = 100;
+
+ /** The maximum number of pixels per second to autoscroll. */
+ private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
+
+ /**
+ * The minimum area where the grid doesn't scroll while the pointer is
+ * pressed.
+ */
+ private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
+
+ /**
+ * This class's main objective is to listen when to stop autoscrolling, and
+ * make sure everything stops accordingly.
+ */
+ private class TouchEventHandler implements NativePreviewHandler {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ switch (event.getTypeInt()) {
+ case Event.ONTOUCHSTART: {
+ if (event.getNativeEvent().getTouches().length() == 1) {
+ /*
+ * Something has dropped a touchend/touchcancel and the
+ * scroller is most probably running amok. Let's cancel it
+ * and pretend that everything's going as expected
+ *
+ * Because this is a preview, this code is run before the
+ * event handler in MultiSelectionRenderer.onBrowserEvent.
+ * Therefore, we can simply kill everything and let that
+ * method restart things as they should.
+ */
+ autoScrollHandler.stop();
+
+ /*
+ * Related TODO: investigate why iOS seems to ignore a
+ * touchend/touchcancel when frames are dropped, and/or if
+ * something can be done about that.
+ */
+ }
+ break;
+ }
+
+ case Event.ONTOUCHMOVE:
+ event.cancel();
+ break;
+
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ /*
+ * Remember: targetElement is always where touchstart started,
+ * not where the finger is pointing currently.
+ */
+ final Element targetElement = Element.as(event.getNativeEvent()
+ .getEventTarget());
+ if (isInFirstColumn(targetElement)) {
+ removeNativeHandler();
+ event.cancel();
+ }
+ break;
+ }
+ }
+
+ private boolean isInFirstColumn(final Element element) {
+ if (element == null) {
+ return false;
+ }
+ final Element tbody = getTbodyElement();
+
+ if (tbody == null || !tbody.isOrHasChild(element)) {
+ return false;
+ }
+
+ /*
+ * The null-parent in the while clause is in the case where element
+ * is an immediate tr child in the tbody. Should never happen in
+ * internal code, but hey...
+ */
+ Element cursor = element;
+ while (cursor.getParentElement() != null
+ && cursor.getParentElement().getParentElement() != tbody) {
+ cursor = cursor.getParentElement();
+ }
+
+ final Element tr = cursor.getParentElement();
+ return tr.getFirstChildElement().equals(cursor);
+ }
+ }
+
+ /**
+ * This class's responsibility is to
+ * <ul>
+ * <li>scroll the table while a pointer is kept in a scrolling zone and
+ * <li>select rows whenever a pointer is "activated" on a selection cell
+ * </ul>
+ * <p>
+ * <em>Techical note:</em> This class is an AnimationCallback because we
+ * need a timer: when the finger is kept in place while the grid scrolls, we
+ * still need to be able to make new selections. So, instead of relying on
+ * events (which won't be fired, since the pointer isn't necessarily
+ * moving), we do this check on each frame while the pointer is "active"
+ * (mouse is pressed, finger is on screen).
+ */
+ private class AutoScrollerAndSelector implements AnimationCallback {
+
+ /**
+ * If the acceleration gradient area is smaller than this, autoscrolling
+ * will be disabled (it becomes too quick to accelerate to be usable).
+ */
+ private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
+
+ /**
+ * The speed at which the gradient area recovers, once scrolling in that
+ * direction has started.
+ */
+ private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1;
+ private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC / 1000.0d;
+
+ /**
+ * The lowest y-coordinate on the {@link Event#getClientY() client} from
+ * where we need to start scrolling towards the top.
+ */
+ private int topBound = -1;
+
+ /**
+ * The highest y-coordinate on the {@link Event#getClientY() client}
+ * from where we need to scrolling towards the bottom.
+ */
+ private int bottomBound = -1;
+
+ /**
+ * <code>true</code> if the pointer is selecting, <code>false</code> if
+ * the pointer is deselecting.
+ */
+ private final boolean selectionPaint;
+
+ /**
+ * The area where the selection acceleration takes place. If &lt;
+ * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
+ */
+ private final int gradientArea;
+
+ /**
+ * The number of pixels per seconds we currently are scrolling (negative
+ * is towards the top, positive is towards the bottom).
+ */
+ private double scrollSpeed = 0;
+
+ private double prevTimestamp = 0;
+
+ /**
+ * This field stores fractions of pixels to scroll, to make sure that
+ * we're able to scroll less than one px per frame.
+ */
+ private double pixelsToScroll = 0.0d;
+
+ /** Should this animator be running. */
+ private boolean running = false;
+
+ /** The handle in which this instance is running. */
+ private AnimationHandle handle;
+
+ /** The pointer's pageX coordinate of the first click. */
+ private int initialPageX = -1;
+
+ /** The pointer's pageY coordinate. */
+ private int pageY;
+
+ /** The logical index of the row that was most recently modified. */
+ private int lastModifiedLogicalRow = -1;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalTopBound;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalBottomBound;
+
+ private boolean scrollAreaShouldRebound = false;
+
+ private final int bodyAbsoluteTop;
+ private final int bodyAbsoluteBottom;
+
+ public AutoScrollerAndSelector(final int topBound,
+ final int bottomBound, final int gradientArea,
+ final boolean selectionPaint) {
+ finalTopBound = topBound;
+ finalBottomBound = bottomBound;
+ this.gradientArea = gradientArea;
+ this.selectionPaint = selectionPaint;
+
+ bodyAbsoluteTop = getBodyClientTop();
+ bodyAbsoluteBottom = getBodyClientBottom();
+ }
+
+ @Override
+ public void execute(final double timestamp) {
+ final double timeDiff = timestamp - prevTimestamp;
+ prevTimestamp = timestamp;
+
+ reboundScrollArea(timeDiff);
+
+ pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
+ final int intPixelsToScroll = (int) pixelsToScroll;
+ pixelsToScroll -= intPixelsToScroll;
+
+ if (intPixelsToScroll != 0) {
+ grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll);
+ }
+
+ int constrainedPageY = Math.max(bodyAbsoluteTop,
+ Math.min(bodyAbsoluteBottom, pageY));
+ int logicalRow = getLogicalRowIndex(WidgetUtil.getElementFromPoint(
+ initialPageX, constrainedPageY));
+
+ int incrementOrDecrement = (logicalRow > lastModifiedLogicalRow) ? 1
+ : -1;
+
+ /*
+ * Both pageY and initialPageX have their initialized (and
+ * unupdated) values while the cursor hasn't moved since the first
+ * invocation. This will lead to logicalRow being -1, until the
+ * pointer has been moved.
+ */
+ while (logicalRow != -1 && lastModifiedLogicalRow != logicalRow) {
+ lastModifiedLogicalRow += incrementOrDecrement;
+ setSelected(lastModifiedLogicalRow, selectionPaint);
+ }
+
+ reschedule();
+ }
+
+ /**
+ * If the scroll are has been offset by the pointer starting out there,
+ * move it back a bit
+ */
+ private void reboundScrollArea(double timeDiff) {
+ if (!scrollAreaShouldRebound) {
+ return;
+ }
+
+ int reboundPx = (int) Math.ceil(SCROLL_AREA_REBOUND_PX_PER_MS
+ * timeDiff);
+ if (topBound < finalTopBound) {
+ topBound += reboundPx;
+ topBound = Math.min(topBound, finalTopBound);
+ updateScrollSpeed(pageY);
+ } else if (bottomBound > finalBottomBound) {
+ bottomBound -= reboundPx;
+ bottomBound = Math.max(bottomBound, finalBottomBound);
+ updateScrollSpeed(pageY);
+ }
+ }
+
+ private void updateScrollSpeed(final int pointerPageY) {
+
+ final double ratio;
+ if (pointerPageY < topBound) {
+ final double distance = pointerPageY - topBound;
+ ratio = Math.max(-1, distance / gradientArea);
+ }
+
+ else if (pointerPageY > bottomBound) {
+ final double distance = pointerPageY - bottomBound;
+ ratio = Math.min(1, distance / gradientArea);
+ }
+
+ else {
+ ratio = 0;
+ }
+
+ scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
+ }
+
+ public void start(int logicalRowIndex) {
+ running = true;
+ setSelected(logicalRowIndex, selectionPaint);
+ lastModifiedLogicalRow = logicalRowIndex;
+ reschedule();
+ }
+
+ public void stop() {
+ running = false;
+
+ if (handle != null) {
+ handle.cancel();
+ handle = null;
+ }
+ }
+
+ private void reschedule() {
+ if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
+ handle = AnimationScheduler.get().requestAnimationFrame(this,
+ grid.getElement());
+ }
+ }
+
+ public void updatePointerCoords(int pageX, int pageY) {
+ doScrollAreaChecks(pageY);
+ updateScrollSpeed(pageY);
+ this.pageY = pageY;
+
+ if (initialPageX == -1) {
+ initialPageX = pageX;
+ }
+ }
+
+ /**
+ * This method checks whether the first pointer event started in an area
+ * that would start scrolling immediately, and does some actions
+ * accordingly.
+ * <p>
+ * If it is, that scroll area will be offset "beyond" the pointer (above
+ * if pointer is towards the top, otherwise below).
+ * <p>
+ * <span style="font-size:smaller">*) This behavior will change in
+ * future patches (henrik paul 2.7.2014)</span>
+ */
+ private void doScrollAreaChecks(int pageY) {
+ /*
+ * The first run makes sure that neither scroll position is
+ * underneath the finger, but offset to either direction from
+ * underneath the pointer.
+ */
+ if (topBound == -1) {
+ topBound = Math.min(finalTopBound, pageY);
+ bottomBound = Math.max(finalBottomBound, pageY);
+ }
+
+ /*
+ * Subsequent runs make sure that the scroll area grows (but doesn't
+ * shrink) with the finger, but no further than the final bound.
+ */
+ else {
+ int oldTopBound = topBound;
+ if (topBound < finalTopBound) {
+ topBound = Math.max(topBound,
+ Math.min(finalTopBound, pageY));
+ }
+
+ int oldBottomBound = bottomBound;
+ if (bottomBound > finalBottomBound) {
+ bottomBound = Math.min(bottomBound,
+ Math.max(finalBottomBound, pageY));
+ }
+
+ final boolean topDidNotMove = oldTopBound == topBound;
+ final boolean bottomDidNotMove = oldBottomBound == bottomBound;
+ final boolean wasVerticalMovement = pageY != this.pageY;
+ scrollAreaShouldRebound = (topDidNotMove && bottomDidNotMove && wasVerticalMovement);
+ }
+ }
+ }
+
+ /**
+ * This class makes sure that pointer movemenets are registered and
+ * delegated to the autoscroller so that it can:
+ * <ul>
+ * <li>modify the speed in which we autoscroll.
+ * <li>"paint" a new row with the selection.
+ * </ul>
+ * Essentially, when a pointer is pressed on the selection column, a native
+ * preview handler is registered (so that selection gestures can happen
+ * outside of the selection column). The handler itself makes sure that it's
+ * detached when the pointer is "lifted".
+ */
+ private class AutoScrollHandler {
+ private AutoScrollerAndSelector autoScroller;
+
+ /** The registration info for {@link #scrollPreviewHandler} */
+ private HandlerRegistration handlerRegistration;
+
+ private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ if (autoScroller == null) {
+ stop();
+ return;
+ }
+
+ final NativeEvent nativeEvent = event.getNativeEvent();
+ int pageY = 0;
+ int pageX = 0;
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent);
+ pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent);
+ autoScroller.updatePointerCoords(pageX, pageY);
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ stop();
+ break;
+ }
+ }
+ };
+
+ /**
+ * The top bound, as calculated from the {@link Event#getClientY()
+ * client} coordinates.
+ */
+ private int topBound = -1;
+
+ /**
+ * The bottom bound, as calculated from the {@link Event#getClientY()
+ * client} coordinates.
+ */
+ private int bottomBound = -1;
+
+ /** The size of the autoscroll acceleration area. */
+ private int gradientArea;
+
+ public void start(int logicalRowIndex) {
+
+ SelectionModel<T> model = grid.getSelectionModel();
+ if (model instanceof Batched) {
+ Batched<?> batchedModel = (Batched<?>) model;
+ batchedModel.startBatchSelect();
+ }
+
+ /*
+ * bounds are updated whenever the autoscroll cycle starts, to make
+ * sure that the widget hasn't changed in size, moved around, or
+ * whatnot.
+ */
+ updateScrollBounds();
+
+ assert handlerRegistration == null : "handlerRegistration was not null";
+ assert autoScroller == null : "autoScroller was not null";
+ handlerRegistration = Event
+ .addNativePreviewHandler(scrollPreviewHandler);
+
+ autoScroller = new AutoScrollerAndSelector(topBound, bottomBound,
+ gradientArea, !isSelected(logicalRowIndex));
+ autoScroller.start(logicalRowIndex);
+ }
+
+ private void updateScrollBounds() {
+ final int topBorder = getBodyClientTop();
+ final int bottomBorder = getBodyClientBottom();
+
+ final int scrollCompensation = getScrollCompensation();
+ topBound = scrollCompensation + topBorder + SCROLL_AREA_GRADIENT_PX;
+ bottomBound = scrollCompensation + bottomBorder
+ - SCROLL_AREA_GRADIENT_PX;
+ gradientArea = SCROLL_AREA_GRADIENT_PX;
+
+ // modify bounds if they're too tightly packed
+ if (bottomBound - topBound < MIN_NO_AUTOSCROLL_AREA_PX) {
+ int adjustment = MIN_NO_AUTOSCROLL_AREA_PX
+ - (bottomBound - topBound);
+ topBound -= adjustment / 2;
+ bottomBound += adjustment / 2;
+ gradientArea -= adjustment / 2;
+ }
+ }
+
+ private int getScrollCompensation() {
+ Element cursor = grid.getElement();
+ int scroll = 0;
+ while (cursor != null) {
+ scroll -= cursor.getScrollTop();
+ cursor = cursor.getParentElement();
+ }
+
+ return scroll;
+ }
+
+ public void stop() {
+ if (handlerRegistration != null) {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+
+ if (autoScroller != null) {
+ autoScroller.stop();
+ autoScroller = null;
+ }
+
+ SelectionModel<T> model = grid.getSelectionModel();
+ if (model instanceof Batched) {
+ Batched<?> batchedModel = (Batched<?>) model;
+ batchedModel.commitBatchSelect();
+ }
+
+ removeNativeHandler();
+ }
+ }
+
+ private static final String LOGICAL_ROW_PROPERTY_INT = "vEscalatorLogicalRow";
+
+ private final Grid<T> grid;
+ private HandlerRegistration nativePreviewHandlerRegistration;
+
+ private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler();
+
+ public MultiSelectionRenderer(final Grid<T> grid) {
+ this.grid = grid;
+ }
+
+ @Override
+ public void destroy() {
+ if (nativePreviewHandlerRegistration != null) {
+ removeNativeHandler();
+ }
+ }
+
+ @Override
+ public void init(RendererCellReference cell) {
+ final InputElement checkbox = InputElement.as(DOM.createInputCheck());
+ cell.getElement().removeAllChildren();
+ cell.getElement().appendChild(checkbox);
+ }
+
+ @Override
+ public void render(final RendererCellReference cell, final Boolean data) {
+ InputElement checkbox = InputElement.as(cell.getElement()
+ .getFirstChildElement());
+ checkbox.setChecked(data.booleanValue());
+ checkbox.setPropertyInt(LOGICAL_ROW_PROPERTY_INT, cell.getRowIndex());
+ }
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ final HashSet<String> events = new HashSet<String>();
+
+ /*
+ * this column's first interest is only to attach a NativePreventHandler
+ * that does all the magic. These events are the beginning of that
+ * cycle.
+ */
+ events.add(BrowserEvents.MOUSEDOWN);
+ events.add(BrowserEvents.TOUCHSTART);
+
+ return events;
+ }
+
+ @Override
+ public boolean onBrowserEvent(final CellReference<?> cell,
+ final NativeEvent event) {
+ if (BrowserEvents.TOUCHSTART.equals(event.getType())
+ || (BrowserEvents.MOUSEDOWN.equals(event.getType()) && event
+ .getButton() == NativeEvent.BUTTON_LEFT)) {
+ injectNativeHandler();
+ int logicalRowIndex = getLogicalRowIndex(Element.as(event
+ .getEventTarget()));
+ autoScrollHandler.start(logicalRowIndex);
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ } else {
+ throw new IllegalStateException("received unexpected event: "
+ + event.getType());
+ }
+ }
+
+ private void injectNativeHandler() {
+ removeNativeHandler();
+ nativePreviewHandlerRegistration = Event
+ .addNativePreviewHandler(new TouchEventHandler());
+ }
+
+ private void removeNativeHandler() {
+ if (nativePreviewHandlerRegistration != null) {
+ nativePreviewHandlerRegistration.removeHandler();
+ nativePreviewHandlerRegistration = null;
+ }
+ }
+
+ private int getLogicalRowIndex(final Element target) {
+ if (target == null) {
+ return -1;
+ }
+
+ /*
+ * We can't simply go backwards until we find a <tr> first element,
+ * because of the table-in-table scenario. We need to, unfortunately, go
+ * up from our known root.
+ */
+ final Element tbody = getTbodyElement();
+ Element tr = tbody.getFirstChildElement();
+ while (tr != null) {
+ if (tr.isOrHasChild(target)) {
+ final Element td = tr.getFirstChildElement();
+ assert td != null : "Cell has disappeared";
+
+ final Element checkbox = td.getFirstChildElement();
+ assert checkbox != null : "Checkbox has disappeared";
+
+ return checkbox.getPropertyInt(LOGICAL_ROW_PROPERTY_INT);
+ }
+ tr = tr.getNextSiblingElement();
+ }
+ return -1;
+ }
+
+ private TableElement getTableElement() {
+ final Element root = grid.getElement();
+ final Element tablewrapper = Element.as(root.getChild(2));
+ if (tablewrapper != null) {
+ return TableElement.as(tablewrapper.getFirstChildElement());
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTbodyElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTBodies().getItem(0);
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTheadElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTHead();
+ } else {
+ return null;
+ }
+ }
+
+ private TableSectionElement getTfootElement() {
+ TableElement table = getTableElement();
+ if (table != null) {
+ return table.getTFoot();
+ } else {
+ return null;
+ }
+ }
+
+ /** Get the "top" of an element in relation to "client" coordinates. */
+ @SuppressWarnings("static-method")
+ private int getClientTop(final Element e) {
+ Element cursor = e;
+ int top = 0;
+ while (cursor != null) {
+ top += cursor.getOffsetTop();
+ cursor = cursor.getOffsetParent();
+ }
+ return top;
+ }
+
+ private int getBodyClientBottom() {
+ return getClientTop(getTfootElement()) - 1;
+ }
+
+ private int getBodyClientTop() {
+ return getClientTop(grid.getElement())
+ + getTheadElement().getOffsetHeight();
+ }
+
+ protected boolean isSelected(final int logicalRow) {
+ return grid.isSelected(grid.getDataSource().getRow(logicalRow));
+ }
+
+ protected void setSelected(final int logicalRow, final boolean select) {
+ T row = grid.getDataSource().getRow(logicalRow);
+ if (select) {
+ grid.select(row);
+ } else {
+ grid.deselect(row);
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SelectionEvent.java b/client/src/com/vaadin/client/widget/grid/selection/SelectionEvent.java
new file mode 100644
index 0000000000..528beb5809
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SelectionEvent.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Event object describing a change in Grid row selection state.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@SuppressWarnings("rawtypes")
+public class SelectionEvent<T> extends GwtEvent<SelectionHandler> {
+
+ private static final Type<SelectionHandler> eventType = new Type<SelectionHandler>();
+
+ private final Grid<T> grid;
+ private final List<T> added;
+ private final List<T> removed;
+ private final boolean batched;
+
+ /**
+ * Creates an event with a single added or removed row.
+ *
+ * @param grid
+ * grid reference, used for getSource
+ * @param added
+ * the added row, or <code>null</code> if a row was not added
+ * @param removed
+ * the removed row, or <code>null</code> if a row was not removed
+ * @param batched
+ * whether or not this selection change event is triggered during
+ * a batched selection/deselection action
+ * @see SelectionModel.Multi.Batched
+ */
+ public SelectionEvent(Grid<T> grid, T added, T removed, boolean batched) {
+ this.grid = grid;
+ this.batched = batched;
+
+ if (added != null) {
+ this.added = Collections.singletonList(added);
+ } else {
+ this.added = Collections.emptyList();
+ }
+
+ if (removed != null) {
+ this.removed = Collections.singletonList(removed);
+ } else {
+ this.removed = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Creates an event where several rows have been added or removed.
+ *
+ * @param grid
+ * Grid reference, used for getSource
+ * @param added
+ * a collection of added rows, or <code>null</code> if no rows
+ * were added
+ * @param removed
+ * a collection of removed rows, or <code>null</code> if no rows
+ * were removed
+ * @param batched
+ * whether or not this selection change event is triggered during
+ * a batched selection/deselection action
+ * @see SelectionModel.Multi.Batched
+ */
+ public SelectionEvent(Grid<T> grid, Collection<T> added,
+ Collection<T> removed, boolean batched) {
+ this.grid = grid;
+ this.batched = batched;
+
+ if (added != null) {
+ this.added = new ArrayList<T>(added);
+ } else {
+ this.added = Collections.emptyList();
+ }
+
+ if (removed != null) {
+ this.removed = new ArrayList<T>(removed);
+ } else {
+ this.removed = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Gets a reference to the Grid object that fired this event.
+ *
+ * @return a grid reference
+ */
+ @Override
+ public Grid<T> getSource() {
+ return grid;
+ }
+
+ /**
+ * Gets all rows added to the selection since the last
+ * {@link SelectionEvent} .
+ *
+ * @return a collection of added rows. Empty collection if no rows were
+ * added.
+ */
+ public Collection<T> getAdded() {
+ return Collections.unmodifiableCollection(added);
+ }
+
+ /**
+ * Gets all rows removed from the selection since the last
+ * {@link SelectionEvent}.
+ *
+ * @return a collection of removed rows. Empty collection if no rows were
+ * removed.
+ */
+ public Collection<T> getRemoved() {
+ return Collections.unmodifiableCollection(removed);
+ }
+
+ /**
+ * Gets currently selected rows.
+ *
+ * @return a non-null collection containing all currently selected rows.
+ */
+ public Collection<T> getSelected() {
+ return grid.getSelectedRows();
+ }
+
+ /**
+ * Gets a type identifier for this event.
+ *
+ * @return a {@link Type} identifier.
+ */
+ public static Type<SelectionHandler> getType() {
+ return eventType;
+ }
+
+ @Override
+ public Type<SelectionHandler> getAssociatedType() {
+ return eventType;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void dispatch(SelectionHandler handler) {
+ handler.onSelect(this);
+ }
+
+ /**
+ * Checks if this selection change event is fired during a batched
+ * selection/deselection operation.
+ *
+ * @return <code>true</code> iff this event is fired during a batched
+ * selection/deselection operation
+ */
+ public boolean isBatchedSelection() {
+ return batched;
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SelectionHandler.java b/client/src/com/vaadin/client/widget/grid/selection/SelectionHandler.java
new file mode 100644
index 0000000000..4f939fa798
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SelectionHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link SelectionEvent}s.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * The row data type
+ * @since 7.4
+ */
+public interface SelectionHandler<T> extends EventHandler {
+
+ /**
+ * Called when a selection model's selection state is changed.
+ *
+ * @param event
+ * a selection event, containing info about rows that have been
+ * added to or removed from the selection.
+ */
+ public void onSelect(SelectionEvent<T> event);
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SelectionModel.java b/client/src/com/vaadin/client/widget/grid/selection/SelectionModel.java
new file mode 100644
index 0000000000..37f6fb48c3
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SelectionModel.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import java.util.Collection;
+
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Common interface for all selection models.
+ * <p>
+ * Selection models perform tracking of selected rows in the Grid, as well as
+ * dispatching events when the selection state changes.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * Grid's row type
+ * @since 7.4
+ */
+public interface SelectionModel<T> {
+
+ /**
+ * Return true if the provided row is considered selected under the
+ * implementing selection model.
+ *
+ * @param row
+ * row object instance
+ * @return <code>true</code>, if the row given as argument is considered
+ * selected.
+ */
+ public boolean isSelected(T row);
+
+ /**
+ * Return the {@link Renderer} responsible for rendering the selection
+ * column.
+ *
+ * @return a renderer instance. If null is returned, a selection column will
+ * not be drawn.
+ */
+ public Renderer<Boolean> getSelectionColumnRenderer();
+
+ /**
+ * Tells this SelectionModel which Grid it belongs to.
+ * <p>
+ * Implementations are free to have this be a no-op. This method is called
+ * internally by Grid.
+ *
+ * @param grid
+ * a {@link Grid} instance; <code>null</code> when removing from
+ * Grid
+ */
+ public void setGrid(Grid<T> grid);
+
+ /**
+ * Resets the SelectionModel to the initial state.
+ * <p>
+ * This method can be called internally, for example, when the attached
+ * Grid's data source changes.
+ */
+ public void reset();
+
+ /**
+ * Returns a Collection containing all selected rows.
+ *
+ * @return a non-null collection.
+ */
+ public Collection<T> getSelectedRows();
+
+ /**
+ * Selection model that allows a maximum of one row to be selected at any
+ * one time.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Single<T> extends SelectionModel<T> {
+
+ /**
+ * Selects a row.
+ *
+ * @param row
+ * a {@link Grid} row object
+ * @return true, if this row as not previously selected.
+ */
+ public boolean select(T row);
+
+ /**
+ * Deselects a row.
+ * <p>
+ * This is a no-op unless {@link row} is the currently selected row.
+ *
+ * @param row
+ * a {@link Grid} row object
+ * @return true, if the currently selected row was deselected.
+ */
+ public boolean deselect(T row);
+
+ /**
+ * Returns the currently selected row.
+ *
+ * @return a {@link Grid} row object or null, if nothing is selected.
+ */
+ public T getSelectedRow();
+
+ }
+
+ /**
+ * Selection model that allows for several rows to be selected at once.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Multi<T> extends SelectionModel<T> {
+
+ /**
+ * A multi selection model that can send selections and deselections in
+ * a batch, instead of committing them one-by-one.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Batched<T> extends Multi<T> {
+ /**
+ * Starts a batch selection.
+ * <p>
+ * Any commands to any select or deselect method will be batched
+ * into one, and a final selection event will be fired when
+ * {@link #commitBatchSelect()} is called.
+ * <p>
+ * <em>Note:</em> {@link SelectionEvent SelectionChangeEvents} will
+ * still be fired for each selection/deselection. You should check
+ * whether the event is a part of a batch or not with
+ * {@link SelectionEvent#isBatchedSelection()}.
+ */
+ public void startBatchSelect();
+
+ /**
+ * Commits and ends a batch selection.
+ * <p>
+ * Any and all selections and deselections since the last invocation
+ * of {@link #startBatchSelect()} will be fired at once as one
+ * collated {@link SelectionEvent}.
+ */
+ public void commitBatchSelect();
+
+ /**
+ * Checks whether or not a batch has been started.
+ *
+ * @return <code>true</code> iff a batch has been started
+ */
+ public boolean isBeingBatchSelected();
+
+ /**
+ * Gets all the rows that would become selected in this batch.
+ *
+ * @return a collection of the rows that would become selected
+ */
+ public Collection<T> getSelectedRowsBatch();
+
+ /**
+ * Gets all the rows that would become deselected in this batch.
+ *
+ * @return a collection of the rows that would become deselected
+ */
+ public Collection<T> getDeselectedRowsBatch();
+ }
+
+ /**
+ * Selects one or more rows.
+ *
+ * @param rows
+ * {@link Grid} row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean select(T... rows);
+
+ /**
+ * Deselects one or more rows.
+ *
+ * @param rows
+ * Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean deselect(T... rows);
+
+ /**
+ * De-selects all rows.
+ *
+ * @return true, if any row was previously selected.
+ */
+ public boolean deselectAll();
+
+ /**
+ * Select all rows in a {@link Collection}.
+ *
+ * @param rows
+ * a collection of Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean select(Collection<T> rows);
+
+ /**
+ * Deselect all rows in a {@link Collection}.
+ *
+ * @param rows
+ * a collection of Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean deselect(Collection<T> rows);
+
+ }
+
+ /**
+ * Interface for a selection model that does not allow anything to be
+ * selected.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface None<T> extends SelectionModel<T> {
+
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SelectionModelMulti.java b/client/src/com/vaadin/client/widget/grid/selection/SelectionModelMulti.java
new file mode 100644
index 0000000000..d654a28b7d
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SelectionModelMulti.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Multi-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SelectionModelMulti<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.Multi.Batched<T> {
+
+ private final LinkedHashSet<RowHandle<T>> selectedRows;
+ private Renderer<Boolean> renderer;
+ private Grid<T> grid;
+
+ private boolean batchStarted = false;
+ private final LinkedHashSet<RowHandle<T>> selectionBatch = new LinkedHashSet<RowHandle<T>>();
+ private final LinkedHashSet<RowHandle<T>> deselectionBatch = new LinkedHashSet<RowHandle<T>>();
+
+ /* Event handling for selection with space key */
+ private SpaceSelectHandler<T> spaceSelectHandler;
+
+ public SelectionModelMulti() {
+ grid = null;
+ renderer = null;
+ selectedRows = new LinkedHashSet<RowHandle<T>>();
+ }
+
+ @Override
+ public boolean isSelected(T row) {
+ return isSelectedByHandle(grid.getDataSource().getHandle(row));
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException(
+ "Selection model is already attached to a grid. "
+ + "Remove the selection model first from "
+ + "the grid and then add it.");
+ }
+
+ this.grid = grid;
+ if (this.grid != null) {
+ spaceSelectHandler = new SpaceSelectHandler<T>(grid);
+ this.renderer = new MultiSelectionRenderer<T>(grid);
+ } else {
+ spaceSelectHandler.removeHandler();
+ spaceSelectHandler = null;
+ this.renderer = null;
+ }
+
+ }
+
+ @Override
+ public boolean select(T... rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+ return select(Arrays.asList(rows));
+ }
+
+ @Override
+ public boolean deselect(T... rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+ return deselect(Arrays.asList(rows));
+ }
+
+ @Override
+ public boolean deselectAll() {
+ if (selectedRows.size() > 0) {
+
+ @SuppressWarnings("unchecked")
+ final LinkedHashSet<RowHandle<T>> selectedRowsClone = (LinkedHashSet<RowHandle<T>>) selectedRows
+ .clone();
+ SelectionEvent<T> event = new SelectionEvent<T>(grid, null,
+ getSelectedRows(), isBeingBatchSelected());
+ selectedRows.clear();
+
+ if (isBeingBatchSelected()) {
+ selectionBatch.clear();
+ deselectionBatch.clear();
+ deselectionBatch.addAll(selectedRowsClone);
+ }
+
+ grid.fireEvent(event);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean select(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+
+ Set<T> added = new LinkedHashSet<T>();
+
+ for (T row : rows) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (selectByHandle(handle)) {
+ added.add(row);
+ }
+ }
+
+ if (added.size() > 0) {
+ grid.fireEvent(new SelectionEvent<T>(grid, added, null,
+ isBeingBatchSelected()));
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deselect(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+
+ Set<T> removed = new LinkedHashSet<T>();
+
+ for (T row : rows) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (deselectByHandle(handle)) {
+ removed.add(row);
+ }
+ }
+
+ if (removed.size() > 0) {
+ grid.fireEvent(new SelectionEvent<T>(grid, null, removed,
+ isBeingBatchSelected()));
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean isSelectedByHandle(RowHandle<T> handle) {
+ return selectedRows.contains(handle);
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (selectedRows.add(handle)) {
+ handle.pin();
+
+ if (isBeingBatchSelected()) {
+ deselectionBatch.remove(handle);
+ selectionBatch.add(handle);
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (selectedRows.remove(handle)) {
+
+ if (!isBeingBatchSelected()) {
+ handle.unpin();
+ } else {
+ selectionBatch.remove(handle);
+ deselectionBatch.add(handle);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ Set<T> selected = new LinkedHashSet<T>();
+ for (RowHandle<T> handle : selectedRows) {
+ selected.add(handle.getRow());
+ }
+ return Collections.unmodifiableSet(selected);
+ }
+
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public void startBatchSelect() {
+ assert !isBeingBatchSelected() : "Batch has already been started";
+ batchStarted = true;
+ }
+
+ @Override
+ public void commitBatchSelect() {
+ assert isBeingBatchSelected() : "Batch was never started";
+ if (!isBeingBatchSelected()) {
+ return;
+ }
+
+ batchStarted = false;
+
+ final Collection<T> added = getSelectedRowsBatch();
+ selectionBatch.clear();
+
+ final Collection<T> removed = getDeselectedRowsBatch();
+
+ // unpin deselected rows
+ for (RowHandle<T> handle : deselectionBatch) {
+ handle.unpin();
+ }
+ deselectionBatch.clear();
+
+ grid.fireEvent(new SelectionEvent<T>(grid, added, removed,
+ isBeingBatchSelected()));
+ }
+
+ @Override
+ public boolean isBeingBatchSelected() {
+ return batchStarted;
+ }
+
+ @Override
+ public Collection<T> getSelectedRowsBatch() {
+ return rowHandlesToRows(selectionBatch);
+ }
+
+ @Override
+ public Collection<T> getDeselectedRowsBatch() {
+ return rowHandlesToRows(deselectionBatch);
+ }
+
+ private ArrayList<T> rowHandlesToRows(Collection<RowHandle<T>> rowHandles) {
+ ArrayList<T> rows = new ArrayList<T>(rowHandles.size());
+ for (RowHandle<T> handle : rowHandles) {
+ rows.add(handle.getRow());
+ }
+ return rows;
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SelectionModelNone.java b/client/src/com/vaadin/client/widget/grid/selection/SelectionModelNone.java
new file mode 100644
index 0000000000..4a8b203a94
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SelectionModelNone.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * No-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SelectionModelNone<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.None<T> {
+
+ @Override
+ public boolean isSelected(T row) {
+ return false;
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return null;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ // noop
+ }
+
+ @Override
+ public void reset() {
+ // noop
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("This selection model "
+ + "does not support selection");
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("This selection model "
+ + "does not support deselection");
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SelectionModelSingle.java b/client/src/com/vaadin/client/widget/grid/selection/SelectionModelSingle.java
new file mode 100644
index 0000000000..20eb3c1e63
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SelectionModelSingle.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * Single-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SelectionModelSingle<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.Single<T> {
+
+ private Grid<T> grid;
+ private RowHandle<T> selectedRow;
+
+ /** Event handling for selection with space key */
+ private SpaceSelectHandler<T> spaceSelectHandler;
+
+ /** Event handling for selection by clicking cells */
+ private ClickSelectHandler<T> clickSelectHandler;
+
+ @Override
+ public boolean isSelected(T row) {
+ return selectedRow != null
+ && selectedRow.equals(grid.getDataSource().getHandle(row));
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ // No Selection column renderer for single selection
+ return null;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException(
+ "Selection model is already attached to a grid. "
+ + "Remove the selection model first from "
+ + "the grid and then add it.");
+ }
+
+ this.grid = grid;
+ if (this.grid != null) {
+ spaceSelectHandler = new SpaceSelectHandler<T>(grid);
+ clickSelectHandler = new ClickSelectHandler<T>(grid);
+ } else {
+ spaceSelectHandler.removeHandler();
+ clickSelectHandler.removeHandler();
+ spaceSelectHandler = null;
+ clickSelectHandler = null;
+ }
+ }
+
+ @Override
+ public boolean select(T row) {
+
+ if (row == null) {
+ throw new IllegalArgumentException("Row cannot be null");
+ }
+
+ T removed = getSelectedRow();
+ if (selectByHandle(grid.getDataSource().getHandle(row))) {
+ grid.fireEvent(new SelectionEvent<T>(grid, row, removed, false));
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deselect(T row) {
+
+ if (row == null) {
+ throw new IllegalArgumentException("Row cannot be null");
+ }
+
+ if (isSelected(row)) {
+ deselectByHandle(selectedRow);
+ grid.fireEvent(new SelectionEvent<T>(grid, null, row, false));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public T getSelectedRow() {
+ return (selectedRow != null ? selectedRow.getRow() : null);
+ }
+
+ @Override
+ public void reset() {
+ if (selectedRow != null) {
+ deselect(getSelectedRow());
+ }
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ if (getSelectedRow() != null) {
+ return Collections.singleton(getSelectedRow());
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (handle != null && !handle.equals(selectedRow)) {
+ deselectByHandle(selectedRow);
+ selectedRow = handle;
+ selectedRow.pin();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (handle != null && handle.equals(selectedRow)) {
+ selectedRow.unpin();
+ selectedRow = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/selection/SpaceSelectHandler.java b/client/src/com/vaadin/client/widget/grid/selection/SpaceSelectHandler.java
new file mode 100644
index 0000000000..7a1bf2dc06
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/selection/SpaceSelectHandler.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.selection;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.widget.grid.DataAvailableEvent;
+import com.vaadin.client.widget.grid.DataAvailableHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyDownHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyUpHandler;
+import com.vaadin.client.widget.grid.events.GridKeyDownEvent;
+import com.vaadin.client.widget.grid.events.GridKeyUpEvent;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+
+/**
+ * Generic class to perform selections when pressing space key.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * row data type
+ * @since 7.4
+ */
+public class SpaceSelectHandler<T> {
+
+ /**
+ * Handler for space key down events in Grid Body
+ */
+ private class SpaceKeyDownHandler implements BodyKeyDownHandler {
+ private HandlerRegistration scrollHandler = null;
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent event) {
+ if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE || spaceDown) {
+ return;
+ }
+
+ // Prevent space page scrolling
+ event.getNativeEvent().preventDefault();
+
+ spaceDown = true;
+ final int rowIndex = event.getFocusedCell().getRowIndex();
+
+ if (scrollHandler != null) {
+ scrollHandler.removeHandler();
+ scrollHandler = null;
+ }
+
+ scrollHandler = grid
+ .addDataAvailableHandler(new DataAvailableHandler() {
+
+ @Override
+ public void onDataAvailable(
+ DataAvailableEvent dataAvailableEvent) {
+ if (dataAvailableEvent.getAvailableRows().contains(
+ rowIndex)) {
+ setSelected(grid, rowIndex);
+ scrollHandler.removeHandler();
+ scrollHandler = null;
+ }
+ }
+ });
+ grid.scrollToRow(rowIndex, ScrollDestination.ANY);
+ }
+
+ protected void setSelected(Grid<T> grid, int rowIndex) {
+ T row = grid.getDataSource().getRow(rowIndex);
+
+ if (grid.isSelected(row)) {
+ grid.deselect(row);
+ } else {
+ grid.select(row);
+ }
+ }
+ }
+
+ private boolean spaceDown = false;
+ private Grid<T> grid;
+ private HandlerRegistration spaceUpHandler;
+ private HandlerRegistration spaceDownHandler;
+
+ /**
+ * Constructor for SpaceSelectHandler. This constructor will add all
+ * necessary handlers for selecting rows with space.
+ *
+ * @param grid
+ * grid to attach to
+ */
+ public SpaceSelectHandler(Grid<T> grid) {
+ this.grid = grid;
+ spaceDownHandler = grid
+ .addBodyKeyDownHandler(new SpaceKeyDownHandler());
+ spaceUpHandler = grid.addBodyKeyUpHandler(new BodyKeyUpHandler() {
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_SPACE) {
+ spaceDown = false;
+ }
+ }
+ });
+ }
+
+ /**
+ * Clean up function for removing all now obsolete handlers.
+ */
+ public void removeHandler() {
+ spaceDownHandler.removeHandler();
+ spaceUpHandler.removeHandler();
+ }
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/widget/grid/sort/Sort.java b/client/src/com/vaadin/client/widget/grid/sort/Sort.java
new file mode 100644
index 0000000000..b1f3c6e39a
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/sort/Sort.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.sort;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Fluid Sort descriptor object.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Sort {
+
+ private final Sort previous;
+ private final SortOrder order;
+ private final int count;
+
+ /**
+ * Basic constructor, used by the {@link #by(GridColumn)} and
+ * {@link #by(GridColumn, SortDirection)} methods.
+ *
+ * @param column
+ * a grid column
+ * @param direction
+ * a sort direction
+ */
+ private Sort(Grid.Column<?, ?> column, SortDirection direction) {
+ previous = null;
+ count = 1;
+ order = new SortOrder(column, direction);
+ }
+
+ /**
+ * Extension constructor. Performs object equality checks on all previous
+ * Sort objects in the chain to make sure that the column being passed in
+ * isn't already used earlier (which would indicate a bug). If the column
+ * has been used before, this constructor throws an
+ * {@link IllegalStateException}.
+ *
+ * @param previous
+ * the sort instance that the new sort instance is to extend
+ * @param column
+ * a (previously unused) grid column reference
+ * @param direction
+ * a sort direction
+ */
+ private Sort(Sort previous, Grid.Column<?, ?> column,
+ SortDirection direction) {
+ this.previous = previous;
+ count = previous.count + 1;
+ order = new SortOrder(column, direction);
+
+ Sort s = previous;
+ while (s != null) {
+ if (s.order.getColumn() == column) {
+ throw new IllegalStateException(
+ "Can not sort along the same column twice");
+ }
+ s = s.previous;
+ }
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column in ascending
+ * order.
+ *
+ * @param column
+ * a grid column object reference
+ * @return a sort instance, typed to the grid data type
+ */
+ public static Sort by(Grid.Column<?, ?> column) {
+ return by(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column.
+ *
+ * @param column
+ * a grid column object reference
+ * @param direction
+ * indicator of sort direction - either ascending or descending
+ * @return a sort instance, typed to the grid data type
+ */
+ public static Sort by(Grid.Column<?, ?> column, SortDirection direction) {
+ return new Sort(column, direction);
+ }
+
+ /**
+ * Continue building a Sort order. The provided column is sorted in
+ * ascending order if the previously added columns have been evaluated as
+ * equals.
+ *
+ * @param column
+ * a grid column object reference
+ * @return a sort instance, typed to the grid data type
+ */
+ public Sort then(Grid.Column<?, ?> column) {
+ return then(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Continue building a Sort order. The provided column is sorted in
+ * specified order if the previously added columns have been evaluated as
+ * equals.
+ *
+ * @param column
+ * a grid column object reference
+ * @param direction
+ * indicator of sort direction - either ascending or descending
+ * @return a sort instance, typed to the grid data type
+ */
+ public Sort then(Grid.Column<?, ?> column, SortDirection direction) {
+ return new Sort(this, column, direction);
+ }
+
+ /**
+ * Build a sort order list. This method is called internally by Grid when
+ * calling {@link com.vaadin.client.ui.grid.Grid#sort(Sort)}, but can also
+ * be called manually to create a SortOrder list, which can also be provided
+ * directly to Grid.
+ *
+ * @return a sort order list.
+ */
+ public List<SortOrder> build() {
+
+ List<SortOrder> order = new ArrayList<SortOrder>(count);
+
+ Sort s = this;
+ for (int i = count - 1; i >= 0; --i) {
+ order.add(0, s.order);
+ s = s.previous;
+ }
+
+ return order;
+ }
+}
diff --git a/client/src/com/vaadin/client/widget/grid/sort/SortEvent.java b/client/src/com/vaadin/client/widget/grid/sort/SortEvent.java
new file mode 100644
index 0000000000..2aad6e4f95
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/sort/SortEvent.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.sort;
+
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.client.widgets.Grid;
+
+/**
+ * A sort event, fired by the Grid when it needs its data source to provide data
+ * sorted in a specific manner.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortEvent<T> extends GwtEvent<SortHandler<?>> {
+
+ private static final Type<SortHandler<?>> TYPE = new Type<SortHandler<?>>();
+
+ private final Grid<T> grid;
+ private final List<SortOrder> order;
+ private final boolean userOriginated;
+
+ /**
+ * Creates a new Sort Event. All provided parameters are final, and passed
+ * on as-is.
+ *
+ * @param grid
+ * a grid reference
+ * @param order
+ * an array dictating the desired sort order of the data source
+ * @param originator
+ * a value indicating where this event originated from
+ */
+ public SortEvent(Grid<T> grid, List<SortOrder> order, boolean userOriginated) {
+ this.grid = grid;
+ this.order = order;
+ this.userOriginated = userOriginated;
+ }
+
+ @Override
+ public Type<SortHandler<?>> getAssociatedType() {
+ return TYPE;
+ }
+
+ /**
+ * Static access to the GWT event type identifier associated with this Event
+ * class
+ *
+ * @return a type object, uniquely describing this event type.
+ */
+ public static Type<SortHandler<?>> getType() {
+ return TYPE;
+ }
+
+ /**
+ * Get access to the Grid that fired this event
+ *
+ * @return the grid instance
+ */
+ @Override
+ public Grid<T> getSource() {
+ return grid;
+ }
+
+ /**
+ * Get access to the Grid that fired this event
+ *
+ * @return the grid instance
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Get the sort ordering that is to be applied to the Grid
+ *
+ * @return a list of sort order objects
+ */
+ public List<SortOrder> getOrder() {
+ return order;
+ }
+
+ /**
+ * Returns whether this event originated from actions done by the user.
+ *
+ * @return true if sort event originated from user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void dispatch(SortHandler<?> handler) {
+ ((SortHandler<T>) handler).sort(this);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/sort/SortHandler.java b/client/src/com/vaadin/client/widget/grid/sort/SortHandler.java
new file mode 100644
index 0000000000..330cbe9d58
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/sort/SortHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.sort;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid sort event, called when the Grid needs its data source to
+ * provide data sorted in a specific manner.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface SortHandler<T> extends EventHandler {
+
+ /**
+ * Handle sorting of the Grid. This method is called when a re-sorting of
+ * the Grid's data is requested.
+ *
+ * @param event
+ * the sort event
+ */
+ public void sort(SortEvent<T> event);
+
+}
diff --git a/client/src/com/vaadin/client/widget/grid/sort/SortOrder.java b/client/src/com/vaadin/client/widget/grid/sort/SortOrder.java
new file mode 100644
index 0000000000..8166f1e6ed
--- /dev/null
+++ b/client/src/com/vaadin/client/widget/grid/sort/SortOrder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widget.grid.sort;
+
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Sort order descriptor. Contains column and direction references.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortOrder {
+
+ private final Grid.Column<?, ?> column;
+ private final SortDirection direction;
+
+ /**
+ * Create a sort order descriptor with a default sorting direction value of
+ * {@link SortDirection#ASCENDING}.
+ *
+ * @param column
+ * a grid column descriptor object
+ */
+ public SortOrder(Grid.Column<?, ?> column) {
+ this(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Create a sort order descriptor.
+ *
+ * @param column
+ * a grid column descriptor object
+ * @param direction
+ * a sorting direction value (ascending or descending)
+ */
+ public SortOrder(Grid.Column<?, ?> column, SortDirection direction) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Grid column reference can not be null!");
+ }
+ if (direction == null) {
+ throw new IllegalArgumentException(
+ "Direction value can not be null!");
+ }
+ this.column = column;
+ this.direction = direction;
+ }
+
+ /**
+ * Returns the {@link GridColumn} reference given in the constructor.
+ *
+ * @return a grid column reference
+ */
+ public Grid.Column<?, ?> getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the {@link SortDirection} value given in the constructor.
+ *
+ * @return a sort direction value
+ */
+ public SortDirection getDirection() {
+ return direction;
+ }
+
+ /**
+ * Returns a new SortOrder object with the sort direction reversed.
+ *
+ * @return a new sort order object
+ */
+ public SortOrder getOpposite() {
+ return new SortOrder(column, direction.getOpposite());
+ }
+}
diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java
new file mode 100644
index 0000000000..4f853a928f
--- /dev/null
+++ b/client/src/com/vaadin/client/widgets/Escalator.java
@@ -0,0 +1,5197 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widgets;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.animation.client.AnimationScheduler;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
+import com.google.gwt.core.client.Duration;
+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.DivElement;
+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.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.Unit;
+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.event.shared.HandlerRegistration;
+import com.google.gwt.logging.client.LogConfiguration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.RequiresResize;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Profiler;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.escalator.FlyweightRow;
+import com.vaadin.client.widget.escalator.PositionFunction;
+import com.vaadin.client.widget.escalator.PositionFunction.AbsolutePosition;
+import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition;
+import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition;
+import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
+import com.vaadin.client.widget.escalator.ScrollbarBundle;
+import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
+import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+
+/*-
+
+ Maintenance Notes! Reading these might save your day.
+ (note for editors: line width is 80 chars, including the
+ one-space indentation)
+
+
+ == Row Container Structure
+
+ AbstractRowContainer
+ |-- AbstractStaticRowContainer
+ | |-- HeaderRowContainer
+ | `-- FooterContainer
+ `---- BodyRowContainer
+
+ AbstractRowContainer is intended to contain all common logic
+ between RowContainers. It manages the bookkeeping of row
+ count, makes sure that all individual cells are rendered
+ the same way, and so on.
+
+ AbstractStaticRowContainer has some special logic that is
+ required by all RowContainers that don't scroll (hence the
+ word "static"). HeaderRowContainer and FooterRowContainer
+ are pretty thin special cases of a StaticRowContainer
+ (mostly relating to positioning of the root element).
+
+ BodyRowContainer could also be split into an additional
+ "AbstractScrollingRowContainer", but I felt that no more
+ inner classes were needed. So it contains both logic
+ required for making things scroll about, and equivalent
+ special cases for layouting, as are found in
+ Header/FooterRowContainers.
+
+
+ == The Three Indices
+
+ Each RowContainer can be thought to have three levels of
+ indices for any given displayed row (but the distinction
+ matters primarily for the BodyRowContainer, because of the
+ way it scrolls through data):
+
+ - Logical index
+ - Physical (or DOM) index
+ - Visual index
+
+ LOGICAL INDEX is the index that is linked to the data
+ source. If you want your data source to represent a SQL
+ database with 10 000 rows, the 7 000:th row in the SQL has a
+ logical index of 6 999, since the index is 0-based (unless
+ that data source does some funky logic).
+
+ PHYSICAL INDEX is the index for a row that you see in a
+ browser's DOM inspector. If your row is the second <tr>
+ element within a <tbody> tag, it has a physical index of 1
+ (because of 0-based indices). In Header and
+ FooterRowContainers, you are safe to assume that the logical
+ index is the same as the physical index. But because the
+ BodyRowContainer never displays large data sources entirely
+ in the DOM, a physical index usually has no apparent direct
+ relationship with its logical index.
+
+ VISUAL INDEX is the index relating to the order that you
+ see a row in, in the browser, as it is rendered. The
+ topmost row is 0, the second is 1, and so on. The visual
+ index is similar to the physical index in the sense that
+ Header and FooterRowContainers can assume a 1:1
+ relationship between visual index and logical index. And
+ again, BodyRowContainer has no such relationship. The
+ body's visual index has additionally no apparent
+ relationship with its physical index. Because the <tr> tags
+ are reused in the body and visually repositioned with CSS
+ as the user scrolls, the relationship between physical
+ index and visual index is quickly broken. You can get an
+ element's visual index via the field
+ BodyRowContainer.visualRowOrder.
+
+ Currently, the physical and visual indices are kept in sync
+ _most of the time_ by a deferred rearrangement of rows.
+ They become desynced when scrolling. This is to help screen
+ readers to read the contents from the DOM in a natural
+ order. See BodyRowContainer.DeferredDomSorter for more
+ about that.
+
+ */
+
+/**
+ * A workaround-class for GWT and JSNI.
+ * <p>
+ * GWT is unable to handle some method calls to Java methods in inner-classes
+ * from within JSNI blocks. Having that inner class extend a non-inner-class (or
+ * implement such an interface), makes it possible for JSNI to indirectly refer
+ * to the inner class, by invoking methods and fields in the non-inner-class
+ * API.
+ *
+ * @see Escalator.Scroller
+ */
+abstract class JsniWorkaround {
+ /**
+ * A JavaScript function that handles the scroll DOM event, and passes it on
+ * to Java code.
+ *
+ * @see #createScrollListenerFunction(Escalator)
+ * @see Escalator#onScroll()
+ * @see Escalator.Scroller#onScroll()
+ */
+ protected final JavaScriptObject scrollListenerFunction;
+
+ /**
+ * A JavaScript function that handles the mousewheel DOM event, and passes
+ * it on to Java code.
+ *
+ * @see #createMousewheelListenerFunction(Escalator)
+ * @see Escalator#onScroll()
+ * @see Escalator.Scroller#onScroll()
+ */
+ protected final JavaScriptObject mousewheelListenerFunction;
+
+ /**
+ * A JavaScript function that handles the touch start DOM event, and passes
+ * it on to Java code.
+ *
+ * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchStartFunction;
+
+ /**
+ * A JavaScript function that handles the touch move DOM event, and passes
+ * it on to Java code.
+ *
+ * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchMoveFunction;
+
+ /**
+ * A JavaScript function that handles the touch end and cancel DOM events,
+ * and passes them on to Java code.
+ *
+ * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchEndFunction;
+
+ protected TouchHandlerBundle touchHandlerBundle;
+
+ protected JsniWorkaround(final Escalator escalator) {
+ scrollListenerFunction = createScrollListenerFunction(escalator);
+ mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
+
+ touchHandlerBundle = new TouchHandlerBundle(escalator);
+ touchStartFunction = touchHandlerBundle.getTouchStartHandler();
+ touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
+ touchEndFunction = touchHandlerBundle.getTouchEndHandler();
+ }
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #scrollListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll()
+ */
+ protected abstract JavaScriptObject createScrollListenerFunction(
+ Escalator esc);
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #mousewheelListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll()
+ */
+ protected abstract JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc);
+}
+
+/**
+ * A low-level table-like widget that features a scrolling virtual viewport and
+ * lazily generated rows.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Escalator extends Widget implements RequiresResize, DeferredWorker {
+
+ // todo comments legend
+ /*
+ * [[optimize]]: There's an opportunity to rewrite the code in such a way
+ * that it _might_ perform better (rememeber to measure, implement,
+ * re-measure)
+ */
+ /*
+ * [[rowheight]]: This code will require alterations that are relevant for
+ * being able to support variable row heights. NOTE: these bits can most
+ * often also be identified by searching for code reading the ROW_HEIGHT_PX
+ * constant.
+ */
+ /*
+ * [[mpixscroll]]: This code will require alterations that are relevant for
+ * supporting the scrolling through more pixels than some browsers normally
+ * would support. (i.e. when we support more than "a million" pixels in the
+ * escalator DOM). NOTE: these bits can most often also be identified by
+ * searching for code that call scrollElem.getScrollTop();.
+ */
+
+ /**
+ * A utility class that contains utility methods that are usually called
+ * from JSNI.
+ * <p>
+ * The methods are moved in this class to minimize the amount of JSNI code
+ * as much as feasible.
+ */
+ static class JsniUtil {
+ public static class TouchHandlerBundle {
+
+ private static final double FLICK_POLL_FREQUENCY = 100d;
+
+ /**
+ * A <a href=
+ * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
+ * >JavaScriptObject overlay</a> for the <a
+ * href="http://www.w3.org/TR/touch-events/">JavaScript
+ * TouchEvent</a> object.
+ * <p>
+ * This needs to be used in the touch event handlers, since GWT's
+ * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
+ * can't be cast from the JSNI call, and the
+ * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
+ * properly populated with the correct values.
+ */
+ private final static class CustomTouchEvent extends
+ JavaScriptObject {
+ protected CustomTouchEvent() {
+ }
+
+ public native NativeEvent getNativeEvent()
+ /*-{
+ return this;
+ }-*/;
+
+ public native int getPageX()
+ /*-{
+ return this.targetTouches[0].pageX;
+ }-*/;
+
+ public native int getPageY()
+ /*-{
+ return this.targetTouches[0].pageY;
+ }-*/;
+ }
+
+ private double touches = 0;
+ private int lastX = 0;
+ private int lastY = 0;
+ private double lastTime = 0;
+ private boolean snappedScrollEnabled = true;
+ private double deltaX = 0;
+ private double deltaY = 0;
+
+ private final Escalator escalator;
+
+ private CustomTouchEvent latestTouchMoveEvent;
+
+ /** The timestamp of {@link #flickPageX1} and {@link #flickPageY1} */
+ private double flickTimestamp = Double.MIN_VALUE;
+
+ /** The most recent flick touch reference Y */
+ private double flickPageY1 = -1;
+ /** The most recent flick touch reference X */
+ private double flickPageX1 = -1;
+
+ /** The previous flick touch reference Y, before {@link #flickPageY1} */
+ private double flickPageY2 = -1;
+ /** The previous flick touch reference X, before {@link #flickPageX1} */
+ private double flickPageX2 = -1;
+
+ /**
+ * This animation callback guarantees the fact that we don't scroll
+ * the grid more than once per visible frame.
+ *
+ * It seems that there will never be more touch events than there
+ * are rendered frames, but there's no guarantee for that. If it was
+ * guaranteed, we probably could do all of this immediately in
+ * {@link #touchMove(CustomTouchEvent)}, instead of deferring it
+ * over here.
+ */
+ private AnimationCallback mover = new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ if (touches != 1) {
+ return;
+ }
+
+ final int x = latestTouchMoveEvent.getPageX();
+ final int y = latestTouchMoveEvent.getPageY();
+
+ /*
+ * Check if we need a new flick coordinate sample (more than
+ * FLICK_POLL_FREQUENCY ms have passed since the last
+ * sample)
+ */
+ if (timestamp - flickTimestamp > FLICK_POLL_FREQUENCY) {
+ flickTimestamp = timestamp;
+ flickPageY2 = flickPageY1;
+ flickPageY1 = y;
+
+ flickPageX2 = flickPageX1;
+ flickPageX1 = x;
+ }
+
+ deltaX = x - lastX;
+ deltaY = y - lastY;
+ lastX = x;
+ lastY = y;
+
+ /*
+ * Instead of using the provided arbitrary timestamp, let's
+ * use a known-format and reproducible timestamp.
+ */
+ lastTime = Duration.currentTimeMillis();
+
+ // snap the scroll to the major axes, at first.
+ if (snappedScrollEnabled) {
+ final double oldDeltaX = deltaX;
+ final double oldDeltaY = deltaY;
+
+ /*
+ * Scrolling snaps to 40 degrees vs. flick scroll's 30
+ * degrees, since slow movements have poor resolution -
+ * it's easy to interpret a slight angle as a steep
+ * angle, since the sample rate is "unnecessarily" high.
+ * 40 simply felt better than 30.
+ */
+ final double[] snapped = Escalator.snapDeltas(deltaX,
+ deltaY, RATIO_OF_40_DEGREES);
+ deltaX = snapped[0];
+ deltaY = snapped[1];
+
+ /*
+ * if the snap failed once, let's follow the pointer
+ * from now on.
+ */
+ if (oldDeltaX != 0 && deltaX == oldDeltaX
+ && oldDeltaY != 0 && deltaY == oldDeltaY) {
+ snappedScrollEnabled = false;
+ }
+ }
+
+ moveScrollFromEvent(escalator, -deltaX, -deltaY,
+ latestTouchMoveEvent.getNativeEvent());
+ }
+ };
+ private AnimationHandle animationHandle;
+
+ public TouchHandlerBundle(final Escalator escalator) {
+ this.escalator = escalator;
+ }
+
+ public native JavaScriptObject getTouchStartHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
+ });
+ }-*/;
+
+ public native JavaScriptObject getTouchMoveHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
+ });
+ }-*/;
+
+ public native JavaScriptObject getTouchEndHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
+ });
+ }-*/;
+
+ public void touchStart(final CustomTouchEvent event) {
+ touches = event.getNativeEvent().getTouches().length();
+ if (touches != 1) {
+ return;
+ }
+
+ escalator.scroller.cancelFlickScroll();
+
+ lastX = event.getPageX();
+ lastY = event.getPageY();
+
+ snappedScrollEnabled = true;
+ }
+
+ public void touchMove(final CustomTouchEvent event) {
+ /*
+ * since we only use the getPageX/Y, and calculate the diff
+ * within the handler, we don't need to calculate any
+ * intermediate deltas.
+ */
+ latestTouchMoveEvent = event;
+
+ if (animationHandle != null) {
+ animationHandle.cancel();
+ }
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(mover, escalator.bodyElem);
+ event.getNativeEvent().preventDefault();
+ }
+
+ public void touchEnd(final CustomTouchEvent event) {
+ touches = event.getNativeEvent().getTouches().length();
+
+ if (touches == 0) {
+
+ /*
+ * We want to smooth the flick calculations here. We have
+ * taken a frame of reference every FLICK_POLL_FREQUENCY.
+ * But if the sample is too fresh, we might introduce noise
+ * in our sampling, so we use the older sample instead. it
+ * might be less accurate, but it's smoother.
+ *
+ * flickPage?1 is the most recent one, while flickPage?2 is
+ * the previous one.
+ */
+
+ final double finalPageY;
+ final double finalPageX;
+ double deltaT = lastTime - flickTimestamp;
+ boolean onlyOneSample = flickPageX2 < 0 || flickPageY2 < 0;
+ if (onlyOneSample || deltaT > FLICK_POLL_FREQUENCY / 3) {
+ finalPageY = flickPageY1;
+ finalPageX = flickPageX1;
+ } else {
+ deltaT += FLICK_POLL_FREQUENCY;
+ finalPageY = flickPageY2;
+ finalPageX = flickPageX2;
+ }
+
+ flickPageY1 = -1;
+ flickPageY2 = -1;
+ flickTimestamp = Double.MIN_VALUE;
+
+ double deltaX = latestTouchMoveEvent.getPageX()
+ - finalPageX;
+ double deltaY = latestTouchMoveEvent.getPageY()
+ - finalPageY;
+
+ escalator.scroller
+ .handleFlickScroll(deltaX, deltaY, deltaT);
+ escalator.body.domSorter.reschedule();
+ }
+ }
+ }
+
+ public static void moveScrollFromEvent(final Escalator escalator,
+ final double deltaX, final double deltaY,
+ final NativeEvent event) {
+
+ if (!Double.isNaN(deltaX)) {
+ escalator.horizontalScrollbar.setScrollPosByDelta(deltaX);
+ }
+
+ if (!Double.isNaN(deltaY)) {
+ escalator.verticalScrollbar.setScrollPosByDelta(deltaY);
+ }
+
+ /*
+ * TODO: only prevent if not scrolled to end/bottom. Or no? UX team
+ * needs to decide.
+ */
+ final boolean warrantedYScroll = deltaY != 0
+ && escalator.verticalScrollbar.showsScrollHandle();
+ final boolean warrantedXScroll = deltaX != 0
+ && escalator.horizontalScrollbar.showsScrollHandle();
+ if (warrantedYScroll || warrantedXScroll) {
+ event.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * The animation callback that handles the animation of a touch-scrolling
+ * flick with inertia.
+ */
+ private class FlickScrollAnimator implements AnimationCallback {
+ private static final double MIN_MAGNITUDE = 0.005;
+ private static final double MAX_SPEED = 7;
+
+ private double velX;
+ private double velY;
+ private double prevTime = 0;
+ private int millisLeft;
+ private double xFric;
+ private double yFric;
+
+ private boolean cancelled = false;
+ private double lastLeft;
+ private double lastTop;
+
+ /**
+ * Creates a new animation callback to handle touch-scrolling flick with
+ * inertia.
+ *
+ * @param deltaX
+ * the last scrolling delta in the x-axis in a touchmove
+ * @param deltaY
+ * the last scrolling delta in the y-axis in a touchmove
+ * @param lastTime
+ * the timestamp of the last touchmove
+ */
+ public FlickScrollAnimator(final double deltaX, final double deltaY,
+ final double deltaT) {
+ velX = Math.max(Math.min(deltaX / deltaT, MAX_SPEED), -MAX_SPEED);
+ velY = Math.max(Math.min(deltaY / deltaT, MAX_SPEED), -MAX_SPEED);
+
+ lastLeft = horizontalScrollbar.getScrollPos();
+ lastTop = verticalScrollbar.getScrollPos();
+
+ /*
+ * If we're scrolling mainly in one of the four major directions,
+ * and only a teeny bit to any other side, snap the scroll to that
+ * major direction instead.
+ */
+ final double[] snapDeltas = Escalator.snapDeltas(velX, velY,
+ RATIO_OF_30_DEGREES);
+ velX = snapDeltas[0];
+ velY = snapDeltas[1];
+
+ if (velX * velX + velY * velY > MIN_MAGNITUDE) {
+ millisLeft = 1500;
+ xFric = velX / millisLeft;
+ yFric = velY / millisLeft;
+ } else {
+ millisLeft = 0;
+ }
+
+ }
+
+ @Override
+ public void execute(final double doNotUseThisTimestamp) {
+ /*
+ * We cannot use the timestamp provided to this method since it is
+ * of a format that cannot be determined at will. Therefore, we need
+ * a timestamp format that we can handle, so our calculations are
+ * correct.
+ */
+
+ if (millisLeft <= 0 || cancelled) {
+ scroller.currentFlickScroller = null;
+ return;
+ }
+
+ final double timestamp = Duration.currentTimeMillis();
+ if (prevTime == 0) {
+ prevTime = timestamp;
+ AnimationScheduler.get().requestAnimationFrame(this);
+ return;
+ }
+
+ double currentLeft = horizontalScrollbar.getScrollPos();
+ double currentTop = verticalScrollbar.getScrollPos();
+
+ final double timeDiff = timestamp - prevTime;
+ double left = currentLeft - velX * timeDiff;
+ setScrollLeft(left);
+ velX -= xFric * timeDiff;
+
+ double top = currentTop - velY * timeDiff;
+ setScrollTop(top);
+ velY -= yFric * timeDiff;
+
+ cancelBecauseOfEdgeOrCornerMaybe();
+
+ prevTime = timestamp;
+ millisLeft -= timeDiff;
+ lastLeft = currentLeft;
+ lastTop = currentTop;
+ AnimationScheduler.get().requestAnimationFrame(this);
+ }
+
+ private void cancelBecauseOfEdgeOrCornerMaybe() {
+ if (lastLeft == horizontalScrollbar.getScrollPos()
+ && lastTop == verticalScrollbar.getScrollPos()) {
+ cancel();
+ }
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+ }
+
+ /**
+ * ScrollDestination case-specific handling logic.
+ */
+ private static double getScrollPos(final ScrollDestination destination,
+ final double targetStartPx, final double targetEndPx,
+ final double viewportStartPx, final double viewportEndPx,
+ final double padding) {
+
+ final double viewportLength = viewportEndPx - viewportStartPx;
+
+ switch (destination) {
+
+ /*
+ * Scroll as little as possible to show the target element. If the
+ * element fits into view, this works as START or END depending on the
+ * current scroll position. If the element does not fit into view, this
+ * works as START.
+ */
+ case ANY: {
+ final double startScrollPos = targetStartPx - padding;
+ final double endScrollPos = targetEndPx + padding - viewportLength;
+
+ if (startScrollPos < viewportStartPx) {
+ return startScrollPos;
+ } else if (targetEndPx + padding > viewportEndPx) {
+ return endScrollPos;
+ } else {
+ // NOOP, it's already visible
+ return viewportStartPx;
+ }
+ }
+
+ /*
+ * Scrolls so that the element is shown at the end of the viewport. The
+ * viewport will, however, not scroll before its first element.
+ */
+ case END: {
+ return targetEndPx + padding - viewportLength;
+ }
+
+ /*
+ * Scrolls so that the element is shown in the middle of the viewport.
+ * The viewport will, however, not scroll beyond its contents, given
+ * more elements than what the viewport is able to show at once. Under
+ * no circumstances will the viewport scroll before its first element.
+ */
+ case MIDDLE: {
+ final double targetMiddle = targetStartPx
+ + (targetEndPx - targetStartPx) / 2;
+ return targetMiddle - viewportLength / 2;
+ }
+
+ /*
+ * Scrolls so that the element is shown at the start of the viewport.
+ * The viewport will, however, not scroll beyond its contents.
+ */
+ case START: {
+ return targetStartPx - padding;
+ }
+
+ /*
+ * Throw an error if we're here. This can only mean that
+ * ScrollDestination has been carelessly amended..
+ */
+ default: {
+ throw new IllegalArgumentException(
+ "Internal: ScrollDestination has been modified, "
+ + "but Escalator.getScrollPos has not been updated "
+ + "to match new values.");
+ }
+ }
+
+ }
+
+ /** An inner class that handles all logic related to scrolling. */
+ private class Scroller extends JsniWorkaround {
+ private double lastScrollTop = 0;
+ private double lastScrollLeft = 0;
+ /**
+ * The current flick scroll animator. This is <code>null</code> if the
+ * view isn't animating a flick scroll at the moment.
+ */
+ private FlickScrollAnimator currentFlickScroller;
+
+ public Scroller() {
+ super(Escalator.this);
+ }
+
+ @Override
+ protected native JavaScriptObject createScrollListenerFunction(
+ Escalator esc)
+ /*-{
+ var vScroll = esc.@com.vaadin.client.widgets.Escalator::verticalScrollbar;
+ var vScrollElem = vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
+
+ var hScroll = esc.@com.vaadin.client.widgets.Escalator::horizontalScrollbar;
+ var hScrollElem = hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
+
+ return $entry(function(e) {
+ var target = e.target || e.srcElement; // IE8 uses e.scrElement
+
+ // in case the scroll event was native (i.e. scrollbars were dragged, or
+ // the scrollTop/Left was manually modified), the bundles have old cache
+ // values. We need to make sure that the caches are kept up to date.
+ if (target === vScrollElem) {
+ vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
+ } else if (target === hScrollElem) {
+ hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()();
+ } else {
+ $wnd.console.error("unexpected scroll target: "+target);
+ }
+ });
+ }-*/;
+
+ @Override
+ protected native JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc)
+ /*-{
+ return $entry(function(e) {
+ var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
+ var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ @com.vaadin.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
+ });
+ }-*/;
+
+ /**
+ * Recalculates the virtual viewport represented by the scrollbars, so
+ * that the sizes of the scroll handles appear correct in the browser
+ */
+ public void recalculateScrollbarsForVirtualViewport() {
+ double scrollContentHeight = body.calculateTotalRowHeight();
+ double scrollContentWidth = columnConfiguration.calculateRowWidth();
+
+ double tableWrapperHeight = heightOfEscalator;
+ double tableWrapperWidth = widthOfEscalator;
+
+ boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ - header.heightOfSection - footer.heightOfSection;
+ boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth;
+
+ // One dimension got scrollbars, but not the other. Recheck time!
+ if (verticalScrollNeeded != horizontalScrollNeeded) {
+ if (!verticalScrollNeeded && horizontalScrollNeeded) {
+ verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ - header.heightOfSection
+ - footer.heightOfSection
+ - horizontalScrollbar.getScrollbarThickness();
+ } else {
+ horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
+ - verticalScrollbar.getScrollbarThickness();
+ }
+ }
+
+ // let's fix the table wrapper size, since it's now stable.
+ if (verticalScrollNeeded) {
+ tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
+ tableWrapperWidth = Math.max(0, tableWrapperWidth);
+ }
+ if (horizontalScrollNeeded) {
+ tableWrapperHeight -= horizontalScrollbar
+ .getScrollbarThickness();
+ tableWrapperHeight = Math.max(0, tableWrapperHeight);
+ }
+ tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
+ tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
+
+ double vScrollbarHeight = Math.max(0, tableWrapperHeight
+ - footer.heightOfSection - header.heightOfSection);
+ verticalScrollbar.setOffsetSize(vScrollbarHeight);
+ verticalScrollbar.setScrollSize(scrollContentHeight);
+
+ /*
+ * If decreasing the amount of frozen columns, and scrolled to the
+ * right, the scroll position might reset. So we need to remember
+ * the scroll position, and re-apply it once the scrollbar size has
+ * been adjusted.
+ */
+ double prevScrollPos = horizontalScrollbar.getScrollPos();
+
+ double unfrozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.between(
+ columnConfiguration.getFrozenColumnCount(),
+ columnConfiguration.getColumnCount()));
+ double frozenPixels = scrollContentWidth - unfrozenPixels;
+ double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
+ horizontalScrollbar.setOffsetSize(hScrollOffsetWidth);
+ horizontalScrollbar.setScrollSize(unfrozenPixels);
+ horizontalScrollbar.getElement().getStyle()
+ .setLeft(frozenPixels, Unit.PX);
+ horizontalScrollbar.setScrollPos(prevScrollPos);
+
+ /*
+ * only show the scrollbar wrapper if the scrollbar itself is
+ * visible.
+ */
+ if (horizontalScrollbar.showsScrollHandle()) {
+ horizontalScrollbarDeco.getStyle().clearDisplay();
+ } else {
+ horizontalScrollbarDeco.getStyle().setDisplay(Display.NONE);
+ }
+
+ /*
+ * only show corner background divs if the vertical scrollbar is
+ * visible.
+ */
+ Style hCornerStyle = headerDeco.getStyle();
+ Style fCornerStyle = footerDeco.getStyle();
+ if (verticalScrollbar.showsScrollHandle()) {
+ hCornerStyle.clearDisplay();
+ fCornerStyle.clearDisplay();
+
+ if (horizontalScrollbar.showsScrollHandle()) {
+ double offset = horizontalScrollbar.getScrollbarThickness();
+ fCornerStyle.setBottom(offset, Unit.PX);
+ } else {
+ fCornerStyle.clearBottom();
+ }
+ } else {
+ hCornerStyle.setDisplay(Display.NONE);
+ fCornerStyle.setDisplay(Display.NONE);
+ }
+ }
+
+ /**
+ * Logical scrolling event handler for the entire widget.
+ */
+ public void onScroll() {
+
+ final double scrollTop = verticalScrollbar.getScrollPos();
+ final double scrollLeft = horizontalScrollbar.getScrollPos();
+ if (lastScrollLeft != scrollLeft) {
+ for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
+ header.updateFreezePosition(i, scrollLeft);
+ body.updateFreezePosition(i, scrollLeft);
+ footer.updateFreezePosition(i, scrollLeft);
+ }
+
+ position.set(headElem, -scrollLeft, 0);
+
+ /*
+ * TODO [[optimize]]: cache this value in case the instanceof
+ * check has undesirable overhead. This could also be a
+ * candidate for some deferred binding magic so that e.g.
+ * AbsolutePosition is not even considered in permutations that
+ * we know support something better. That would let the compiler
+ * completely remove the entire condition since it knows that
+ * the if will never be true.
+ */
+ if (position instanceof AbsolutePosition) {
+ /*
+ * we don't want to put "top: 0" on the footer, since it'll
+ * render wrong, as we already have
+ * "bottom: $footer-height".
+ */
+ footElem.getStyle().setLeft(-scrollLeft, Unit.PX);
+ } else {
+ position.set(footElem, -scrollLeft, 0);
+ }
+
+ lastScrollLeft = scrollLeft;
+ }
+
+ body.setBodyScrollPosition(scrollLeft, scrollTop);
+
+ lastScrollTop = scrollTop;
+ body.updateEscalatorRowsOnScroll();
+ /*
+ * TODO [[optimize]]: Might avoid a reflow by first calculating new
+ * scrolltop and scrolleft, then doing the escalator magic based on
+ * those numbers and only updating the positions after that.
+ */
+ }
+
+ public native void attachScrollListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.attachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void detachScrollListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.removeEventListener("scroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.detachEvent("onscroll", this.@com.vaadin.client.widgets.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void attachMousewheelListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.addEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.attachEvent("onmousewheel", this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void detachMousewheelListener(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.removeEventListener(eventName, this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.detachEvent("onmousewheel", this.@com.vaadin.client.widgets.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void attachTouchListeners(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
+ element.addEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
+ element.addEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ element.addEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ } else {
+ // this would be IE8, but we don't support it with touch
+ }
+ }-*/;
+
+ public native void detachTouchListeners(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.removeEventListener) {
+ element.removeEventListener("touchstart", this.@com.vaadin.client.widgets.JsniWorkaround::touchStartFunction);
+ element.removeEventListener("touchmove", this.@com.vaadin.client.widgets.JsniWorkaround::touchMoveFunction);
+ element.removeEventListener("touchend", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ element.removeEventListener("touchcancel", this.@com.vaadin.client.widgets.JsniWorkaround::touchEndFunction);
+ } else {
+ // this would be IE8, but we don't support it with touch
+ }
+ }-*/;
+
+ private void cancelFlickScroll() {
+ if (currentFlickScroller != null) {
+ currentFlickScroller.cancel();
+ }
+ }
+
+ /**
+ * Handles a touch-based flick scroll.
+ *
+ * @param deltaX
+ * the last scrolling delta in the x-axis in a touchmove
+ * @param deltaY
+ * the last scrolling delta in the y-axis in a touchmove
+ * @param lastTime
+ * the timestamp of the last touchmove
+ */
+ public void handleFlickScroll(double deltaX, double deltaY,
+ double deltaT) {
+ currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY,
+ deltaT);
+ AnimationScheduler.get()
+ .requestAnimationFrame(currentFlickScroller);
+ }
+
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding) {
+ assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
+
+ /*
+ * To cope with frozen columns, we just pretend those columns are
+ * not there at all when calculating the position of the target
+ * column and the boundaries of the viewport. The resulting
+ * scrollLeft will be correct without compensation since the DOM
+ * structure effectively means that scrollLeft also ignores the
+ * frozen columns.
+ */
+ final double frozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0,
+ columnConfiguration.frozenColumns));
+
+ final double targetStartPx = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
+ - frozenPixels;
+ final double targetEndPx = targetStartPx
+ + columnConfiguration.getColumnWidthActual(columnIndex);
+
+ final double viewportStartPx = getScrollLeft();
+ double viewportEndPx = viewportStartPx
+ + WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(getElement())
+ - frozenPixels;
+ if (verticalScrollbar.showsScrollHandle()) {
+ viewportEndPx -= WidgetUtil.getNativeScrollbarSize();
+ }
+
+ final double scrollLeft = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ /*
+ * note that it doesn't matter if the scroll would go beyond the
+ * content, since the browser will adjust for that, and everything
+ * fall into line accordingly.
+ */
+ setScrollLeft(scrollLeft);
+ }
+
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final double padding) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final double targetStartPx = body.getDefaultRowHeight() * rowIndex;
+ final double targetEndPx = targetStartPx
+ + body.getDefaultRowHeight();
+
+ final double viewportStartPx = getScrollTop();
+ final double viewportEndPx = viewportStartPx
+ + body.calculateHeight();
+
+ final double scrollTop = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ /*
+ * note that it doesn't matter if the scroll would go beyond the
+ * content, since the browser will adjust for that, and everything
+ * falls into line accordingly.
+ */
+ setScrollTop(scrollTop);
+ }
+ }
+
+ private class ColumnAutoWidthAssignScheduler {
+ private boolean isScheduled = false;
+ private final ScheduledCommand widthCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (!isScheduled) {
+ return;
+ }
+
+ isScheduled = false;
+
+ ColumnConfigurationImpl cc = columnConfiguration;
+ for (int col = 0; col < cc.getColumnCount(); col++) {
+ ColumnConfigurationImpl.Column column = cc.columns.get(col);
+ if (!column.isWidthFinalized()) {
+ cc.setColumnWidth(col, -1);
+ column.widthIsFinalized();
+ }
+ }
+ }
+ };
+
+ /**
+ * Calculates the widths of all uncalculated cells once the javascript
+ * execution is done.
+ * <p>
+ * This method makes sure that any duplicate requests in the same cycle
+ * are ignored.
+ */
+ public void reschedule() {
+ if (!isScheduled) {
+ isScheduled = true;
+ Scheduler.get().scheduleFinally(widthCommand);
+ }
+ }
+
+ public void cancel() {
+ isScheduled = false;
+ }
+ }
+
+ protected abstract class AbstractRowContainer implements RowContainer {
+ private EscalatorUpdater updater = EscalatorUpdater.NULL;
+
+ private int rows;
+
+ /**
+ * The table section element ({@code <thead>}, {@code <tbody>} or
+ * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
+ */
+ protected final TableSectionElement root;
+
+ /** The height of the combined rows in the DOM. Never negative. */
+ protected double heightOfSection = 0;
+
+ /**
+ * The primary style name of the escalator. Most commonly provided by
+ * Escalator as "v-escalator".
+ */
+ private String primaryStyleName = null;
+
+ /**
+ * A map containing cached values of an element's current top position.
+ * <p>
+ * Don't use this field directly, because it will not take proper care
+ * of all the bookkeeping required.
+ *
+ * @deprecated Use {@link #setRowPosition(Element, int, int)},
+ * {@link #getRowTop(Element)} and
+ * {@link #removeRowPosition(Element)} instead.
+ */
+ @Deprecated
+ private final Map<TableRowElement, Double> rowTopPositionMap = new HashMap<TableRowElement, Double>();
+
+ private boolean defaultRowHeightShouldBeAutodetected = true;
+
+ private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
+
+ public AbstractRowContainer(
+ final TableSectionElement rowContainerElement) {
+ root = rowContainerElement;
+ }
+
+ @Override
+ public Element getElement() {
+ return root;
+ }
+
+ /**
+ * Gets the tag name of an element to represent a cell in a row.
+ * <p>
+ * Usually {@code "th"} or {@code "td"}.
+ * <p>
+ * <em>Note:</em> To actually <em>create</em> such an element, use
+ * {@link #createCellElement(int, int)} instead.
+ *
+ * @return the tag name for the element to represent cells as
+ * @see #createCellElement(int, int)
+ */
+ protected abstract String getCellElementTagName();
+
+ @Override
+ public EscalatorUpdater getEscalatorUpdater() {
+ return updater;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows or columns
+ * when this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) {
+ if (escalatorUpdater == null) {
+ throw new IllegalArgumentException(
+ "escalator updater cannot be null");
+ }
+
+ updater = escalatorUpdater;
+
+ if (hasColumnAndRowData() && getRowCount() > 0) {
+ refreshRows(0, getRowCount());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeRows(final int index, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfRows);
+
+ rows -= numberOfRows;
+
+ if (!isAttached()) {
+ return;
+ }
+
+ if (hasSomethingInDom()) {
+ paintRemoveRows(index, numberOfRows);
+ }
+ }
+
+ /**
+ * Removes those row elements from the DOM that correspond to the given
+ * range of logical indices. This may be fewer than {@code numberOfRows}
+ * , even zero, if not all the removed rows are actually visible.
+ * <p>
+ * The implementation must call {@link #paintRemoveRow(Element, int)}
+ * for each row that is removed from the DOM.
+ *
+ * @param index
+ * the logical index of the first removed row
+ * @param numberOfRows
+ * number of logical rows to remove
+ */
+ protected abstract void paintRemoveRows(final int index,
+ final int numberOfRows);
+
+ /**
+ * Removes a row element from the DOM, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
+ * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
+ * and after removing the row, respectively.
+ * <p>
+ * This method must be called for each removed DOM row by any
+ * {@link #paintRemoveRows(int, int)} implementation.
+ *
+ * @param tr
+ * the row element to remove.
+ */
+ protected void paintRemoveRow(final TableRowElement tr,
+ final int logicalRowIndex) {
+
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ getEscalatorUpdater().preDetach(flyweightRow,
+ flyweightRow.getCells());
+
+ tr.removeFromParent();
+
+ getEscalatorUpdater().postDetach(flyweightRow,
+ flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+
+ }
+
+ protected void assertArgumentsAreValidAndWithinRange(final int index,
+ final int numberOfRows) throws IllegalArgumentException,
+ IndexOutOfBoundsException {
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ if (index < 0 || index + numberOfRows > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "row range (" + index + ".." + (index + numberOfRows)
+ + ") was outside of the current number of rows ("
+ + getRowCount() + ")");
+ }
+ }
+
+ @Override
+ public int getRowCount() {
+ return rows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertRows(final int index, final int numberOfRows) {
+ if (index < 0 || index > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given index (" + index
+ + ") was outside of the current number of rows (0.."
+ + getRowCount() + ")");
+ }
+
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ rows += numberOfRows;
+
+ /*
+ * only add items in the DOM if the widget itself is attached to the
+ * DOM. We can't calculate sizes otherwise.
+ */
+ if (isAttached()) {
+ paintInsertRows(index, numberOfRows);
+
+ if (rows == numberOfRows) {
+ /*
+ * We are inserting the first rows in this container. We
+ * potentially need to autocalculate the widths for the
+ * cells for the first time.
+ *
+ * To make sure that can take the entire dataset into
+ * account, we'll do this deferredly, so that each container
+ * section gets populated before we start calculating.
+ */
+ columnAutoWidthAssignScheduler.reschedule();
+ }
+ }
+ }
+
+ /**
+ * Actually add rows into the DOM, now that everything can be
+ * calculated.
+ *
+ * @param visualIndex
+ * the DOM index to add rows into
+ * @param numberOfRows
+ * the number of rows to insert
+ * @return a list of the added row elements
+ */
+ protected abstract void paintInsertRows(final int visualIndex,
+ final int numberOfRows);
+
+ protected List<TableRowElement> paintInsertStaticRows(
+ final int visualIndex, final int numberOfRows) {
+ assert isAttached() : "Can't paint rows if Escalator is not attached";
+
+ final List<TableRowElement> addedRows = new ArrayList<TableRowElement>();
+
+ if (numberOfRows < 1) {
+ return addedRows;
+ }
+
+ Node referenceRow;
+ if (root.getChildCount() != 0 && visualIndex != 0) {
+ // get the row node we're inserting stuff after
+ referenceRow = root.getChild(visualIndex - 1);
+ } else {
+ // index is 0, so just prepend.
+ referenceRow = null;
+ }
+
+ for (int row = visualIndex; row < visualIndex + numberOfRows; row++) {
+ final TableRowElement tr = TableRowElement.as(DOM.createTR());
+ addedRows.add(tr);
+ tr.addClassName(getStylePrimaryName() + "-row");
+
+ for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
+ final double colWidth = columnConfiguration
+ .getColumnWidthActual(col);
+ final TableCellElement cellElem = createCellElement(colWidth);
+ tr.appendChild(cellElem);
+
+ // Set stylename and position if new cell is frozen
+ if (col < columnConfiguration.frozenColumns) {
+ cellElem.addClassName("frozen");
+ position.set(cellElem, scroller.lastScrollLeft, 0);
+ }
+ }
+
+ referenceRow = paintInsertRow(referenceRow, tr, row);
+ }
+ reapplyRowWidths();
+
+ recalculateSectionHeight();
+
+ return addedRows;
+ }
+
+ /**
+ * Inserts a single row into the DOM, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
+ * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
+ * and after inserting the row, respectively. The row should have its
+ * cells already inserted.
+ *
+ * @param referenceRow
+ * the row after which to insert or null if insert as first
+ * @param tr
+ * the row to be inserted
+ * @param logicalRowIndex
+ * the logical index of the inserted row
+ * @return the inserted row to be used as the new reference
+ */
+ protected Node paintInsertRow(Node referenceRow,
+ final TableRowElement tr, int logicalRowIndex) {
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ getEscalatorUpdater().preAttach(flyweightRow,
+ flyweightRow.getCells());
+
+ referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
+ referenceRow);
+
+ getEscalatorUpdater().postAttach(flyweightRow,
+ flyweightRow.getCells());
+ updater.update(flyweightRow, flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+ return referenceRow;
+ }
+
+ private Node insertAfterReferenceAndUpdateIt(final Element parent,
+ final Element elem, final Node referenceNode) {
+ if (referenceNode != null) {
+ parent.insertAfter(elem, referenceNode);
+ } else {
+ /*
+ * referencenode being null means we have offset 0, i.e. make it
+ * the first row
+ */
+ /*
+ * TODO [[optimize]]: Is insertFirst or append faster for an
+ * empty root?
+ */
+ parent.insertFirst(elem);
+ }
+ return elem;
+ }
+
+ abstract protected void recalculateSectionHeight();
+
+ /**
+ * Returns the height of all rows in the row container.
+ */
+ protected double calculateTotalRowHeight() {
+ return getDefaultRowHeight() * getRowCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ // overridden because of JavaDoc
+ public void refreshRows(final int index, final int numberOfRows) {
+ Range rowRange = Range.withLength(index, numberOfRows);
+ Range colRange = Range.withLength(0, getColumnConfiguration()
+ .getColumnCount());
+ refreshCells(rowRange, colRange);
+ }
+
+ protected abstract void refreshCells(Range logicalRowRange,
+ Range colRange);
+
+ void refreshRow(TableRowElement tr, int logicalRowIndex) {
+ refreshRow(tr, logicalRowIndex, Range.withLength(0,
+ getColumnConfiguration().getColumnCount()));
+ }
+
+ void refreshRow(final TableRowElement tr, final int logicalRowIndex,
+ Range colRange) {
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+ Iterable<FlyweightCell> cellsToUpdate = flyweightRow.getCells(
+ colRange.getStart(), colRange.length());
+ updater.update(flyweightRow, cellsToUpdate);
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+ }
+
+ /**
+ * Create and setup an empty cell element.
+ *
+ * @param width
+ * the width of the cell, in pixels
+ *
+ * @return a set-up empty cell element
+ */
+ public TableCellElement createCellElement(final double width) {
+ final TableCellElement cellElem = TableCellElement.as(DOM
+ .createElement(getCellElementTagName()));
+
+ final double height = getDefaultRowHeight();
+ assert height >= 0 : "defaultRowHeight was negative. There's a setter leak somewhere.";
+ cellElem.getStyle().setHeight(height, Unit.PX);
+
+ if (width >= 0) {
+ cellElem.getStyle().setWidth(width, Unit.PX);
+ }
+ cellElem.addClassName(getStylePrimaryName() + "-cell");
+ return cellElem;
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index) {
+ return getTrByVisualIndex(index);
+ }
+
+ /**
+ * Gets the child element that is visually at a certain index
+ *
+ * @param index
+ * the index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within {@link #root}
+ */
+ protected abstract TableRowElement getTrByVisualIndex(int index)
+ throws IndexOutOfBoundsException;
+
+ protected void paintRemoveColumns(final int offset,
+ final int numberOfColumns) {
+ for (int i = 0; i < root.getChildCount(); i++) {
+ TableRowElement row = getTrByVisualIndex(i);
+ flyweightRow.setup(row, i,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ Iterable<FlyweightCell> attachedCells = flyweightRow.getCells(
+ offset, numberOfColumns);
+ getEscalatorUpdater().preDetach(flyweightRow, attachedCells);
+
+ for (int j = 0; j < numberOfColumns; j++) {
+ row.getCells().getItem(offset).removeFromParent();
+ }
+
+ Iterable<FlyweightCell> detachedCells = flyweightRow
+ .getUnattachedCells(offset, numberOfColumns);
+ getEscalatorUpdater().postDetach(flyweightRow, detachedCells);
+
+ assert flyweightRow.teardown();
+ }
+ }
+
+ protected void paintInsertColumns(final int offset,
+ final int numberOfColumns, boolean frozen) {
+
+ for (int row = 0; row < root.getChildCount(); row++) {
+ final TableRowElement tr = getTrByVisualIndex(row);
+ paintInsertCells(tr, row, offset, numberOfColumns);
+ }
+ reapplyRowWidths();
+
+ if (frozen) {
+ for (int col = offset; col < offset + numberOfColumns; col++) {
+ setColumnFrozen(col, true);
+ }
+ }
+ }
+
+ /**
+ * Inserts new cell elements into a single row element, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
+ * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
+ * and after inserting the cells, respectively.
+ * <p>
+ * Precondition: The row must be already attached to the DOM and the
+ * FlyweightCell instances corresponding to the new columns added to
+ * {@code flyweightRow}.
+ *
+ * @param tr
+ * the row in which to insert the cells
+ * @param logicalRowIndex
+ * the index of the row
+ * @param offset
+ * the index of the first cell
+ * @param numberOfCells
+ * the number of cells to insert
+ */
+ private void paintInsertCells(final TableRowElement tr,
+ int logicalRowIndex, final int offset, final int numberOfCells) {
+
+ assert root.isOrHasChild(tr) : "The row must be attached to the document";
+
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ Iterable<FlyweightCell> cells = flyweightRow.getUnattachedCells(
+ offset, numberOfCells);
+
+ for (FlyweightCell cell : cells) {
+ final double colWidth = columnConfiguration
+ .getColumnWidthActual(cell.getColumn());
+ final TableCellElement cellElem = createCellElement(colWidth);
+ cell.setElement(cellElem);
+ }
+
+ getEscalatorUpdater().preAttach(flyweightRow, cells);
+
+ Node referenceCell;
+ if (offset != 0) {
+ referenceCell = tr.getChild(offset - 1);
+ } else {
+ referenceCell = null;
+ }
+
+ for (FlyweightCell cell : cells) {
+ referenceCell = insertAfterReferenceAndUpdateIt(tr,
+ cell.getElement(), referenceCell);
+ }
+
+ getEscalatorUpdater().postAttach(flyweightRow, cells);
+ getEscalatorUpdater().update(flyweightRow, cells);
+
+ assert flyweightRow.teardown();
+ }
+
+ public void setColumnFrozen(int column, boolean frozen) {
+ final NodeList<TableRowElement> childRows = root.getRows();
+
+ for (int row = 0; row < childRows.getLength(); row++) {
+ final TableRowElement tr = childRows.getItem(row);
+
+ TableCellElement cell = tr.getCells().getItem(column);
+ if (frozen) {
+ cell.addClassName("frozen");
+ } else {
+ cell.removeClassName("frozen");
+ position.reset(cell);
+ }
+ }
+
+ if (frozen) {
+ updateFreezePosition(column, scroller.lastScrollLeft);
+ }
+ }
+
+ public void updateFreezePosition(int column, double scrollLeft) {
+ final NodeList<TableRowElement> childRows = root.getRows();
+
+ for (int row = 0; row < childRows.getLength(); row++) {
+ final TableRowElement tr = childRows.getItem(row);
+
+ TableCellElement cell = tr.getCells().getItem(column);
+ position.set(cell, scrollLeft, 0);
+ }
+ }
+
+ /**
+ * Iterates through all the cells in a column and returns the width of
+ * the widest element in this RowContainer.
+ *
+ * @param index
+ * the index of the column to inspect
+ * @return the pixel width of the widest element in the indicated column
+ */
+ public double calculateMaxColWidth(int index) {
+ TableRowElement row = TableRowElement.as(root
+ .getFirstChildElement());
+ double maxWidth = 0;
+ while (row != null) {
+ final TableCellElement cell = row.getCells().getItem(index);
+ final boolean isVisible = !cell.getStyle().getDisplay()
+ .equals(Display.NONE.getCssName());
+ if (isVisible) {
+ maxWidth = Math.max(maxWidth, WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(cell));
+ }
+ row = TableRowElement.as(row.getNextSiblingElement());
+ }
+ return maxWidth;
+ }
+
+ /**
+ * Reapplies all the cells' widths according to the calculated widths in
+ * the column configuration.
+ */
+ public void reapplyColumnWidths() {
+ Element row = root.getFirstChildElement();
+ while (row != null) {
+ Element cell = row.getFirstChildElement();
+ int columnIndex = 0;
+ while (cell != null) {
+ final double width = getCalculatedColumnWidthWithColspan(
+ cell, columnIndex);
+
+ /*
+ * TODO Should Escalator implement ProvidesResize at some
+ * point, this is where we need to do that.
+ */
+ cell.getStyle().setWidth(width, Unit.PX);
+
+ cell = cell.getNextSiblingElement();
+ columnIndex++;
+ }
+ row = row.getNextSiblingElement();
+ }
+
+ reapplyRowWidths();
+ }
+
+ private double getCalculatedColumnWidthWithColspan(final Element cell,
+ final int columnIndex) {
+ final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
+ Range spannedColumns = Range.withLength(columnIndex, colspan);
+
+ /*
+ * Since browsers don't explode with overflowing colspans, escalator
+ * shouldn't either.
+ */
+ if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) {
+ spannedColumns = Range.between(columnIndex,
+ columnConfiguration.getColumnCount());
+ }
+ return columnConfiguration
+ .getCalculatedColumnsWidth(spannedColumns);
+ }
+
+ /**
+ * Applies the total length of the columns to each row element.
+ * <p>
+ * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
+ * method only modifies the width of the {@code <tr>} element, not the
+ * cells within.
+ */
+ protected void reapplyRowWidths() {
+ double rowWidth = columnConfiguration.calculateRowWidth();
+
+ com.google.gwt.dom.client.Element row = root.getFirstChildElement();
+ while (row != null) {
+ if (rowWidth >= 0) {
+ row.getStyle().setWidth(rowWidth, Unit.PX);
+ }
+ row = row.getNextSiblingElement();
+ }
+ }
+
+ /**
+ * The primary style name for the container.
+ *
+ * @param primaryStyleName
+ * the style name to use as prefix for all row and cell style
+ * names.
+ */
+ protected void setStylePrimaryName(String primaryStyleName) {
+ String oldStyle = getStylePrimaryName();
+ if (SharedUtil.equals(oldStyle, primaryStyleName)) {
+ return;
+ }
+
+ this.primaryStyleName = primaryStyleName;
+
+ // Update already rendered rows and cells
+ Element row = root.getRows().getItem(0);
+ while (row != null) {
+ UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
+ Element cell = TableRowElement.as(row).getCells().getItem(0);
+ while (cell != null) {
+ assert TableCellElement.is(cell);
+ UIObject.setStylePrimaryName(cell, primaryStyleName
+ + "-cell");
+ cell = cell.getNextSiblingElement();
+ }
+ row = row.getNextSiblingElement();
+ }
+ }
+
+ /**
+ * Returns the primary style name of the container.
+ *
+ * @return The primary style name or <code>null</code> if not set.
+ */
+ protected String getStylePrimaryName() {
+ return primaryStyleName;
+ }
+
+ @Override
+ public void setDefaultRowHeight(double px)
+ throws IllegalArgumentException {
+ if (px < 1) {
+ throw new IllegalArgumentException("Height must be positive. "
+ + px + " was given.");
+ }
+
+ defaultRowHeightShouldBeAutodetected = false;
+ defaultRowHeight = px;
+ reapplyDefaultRowHeights();
+ }
+
+ @Override
+ public double getDefaultRowHeight() {
+ return defaultRowHeight;
+ }
+
+ /**
+ * The default height of rows has (most probably) changed.
+ * <p>
+ * Make sure that the displayed rows with a default height are updated
+ * in height and top position.
+ * <p>
+ * <em>Note:</em>This implementation should not call
+ * {@link Escalator#recalculateElementSizes()} - it is done by the
+ * discretion of the caller of this method.
+ */
+ protected abstract void reapplyDefaultRowHeights();
+
+ protected void reapplyRowHeight(final TableRowElement tr,
+ final double heightPx) {
+ assert heightPx >= 0 : "Height must not be negative";
+
+ Element cellElem = tr.getFirstChildElement();
+ while (cellElem != null) {
+ cellElem.getStyle().setHeight(heightPx, Unit.PX);
+ cellElem = cellElem.getNextSiblingElement();
+ }
+
+ /*
+ * no need to apply height to tr-element, it'll be resized
+ * implicitly.
+ */
+ }
+
+ @SuppressWarnings("boxing")
+ protected void setRowPosition(final TableRowElement tr, final int x,
+ final double y) {
+ position.set(tr, x, y);
+ rowTopPositionMap.put(tr, y);
+ }
+
+ @SuppressWarnings("boxing")
+ protected double getRowTop(final TableRowElement tr) {
+ return rowTopPositionMap.get(tr);
+ }
+
+ protected void removeRowPosition(TableRowElement tr) {
+ rowTopPositionMap.remove(tr);
+ }
+
+ public void autodetectRowHeightLater() {
+ Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (defaultRowHeightShouldBeAutodetected && isAttached()) {
+ autodetectRowHeightNow();
+ defaultRowHeightShouldBeAutodetected = false;
+ }
+ }
+ });
+ }
+
+ public void autodetectRowHeightNow() {
+ if (!isAttached()) {
+ // Run again when attached
+ defaultRowHeightShouldBeAutodetected = true;
+ return;
+ }
+
+ final Element detectionTr = DOM.createTR();
+ detectionTr.setClassName(getStylePrimaryName() + "-row");
+
+ final Element cellElem = DOM.createElement(getCellElementTagName());
+ cellElem.setClassName(getStylePrimaryName() + "-cell");
+ cellElem.setInnerText("Ij");
+
+ detectionTr.appendChild(cellElem);
+ root.appendChild(detectionTr);
+ double boundingHeight = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(cellElem);
+ defaultRowHeight = Math.max(1.0d, boundingHeight);
+ root.removeChild(detectionTr);
+
+ if (root.hasChildNodes()) {
+ reapplyDefaultRowHeights();
+ applyHeightByRows();
+ }
+ }
+
+ @Override
+ public Cell getCell(final Element element) {
+ if (element == null) {
+ throw new IllegalArgumentException("Element cannot be null");
+ }
+
+ /*
+ * Ensure that element is not root nor the direct descendant of root
+ * (a row) and ensure the element is inside the dom hierarchy of the
+ * root element. If not, return.
+ */
+ if (root == element || element.getParentElement() == root
+ || !root.isOrHasChild(element)) {
+ return null;
+ }
+
+ /*
+ * Ensure element is the cell element by iterating up the DOM
+ * hierarchy until reaching cell element.
+ */
+ Element cellElementCandidate = element;
+ while (cellElementCandidate.getParentElement().getParentElement() != root) {
+ cellElementCandidate = cellElementCandidate.getParentElement();
+ }
+ final TableCellElement cellElement = TableCellElement
+ .as(cellElementCandidate);
+
+ // Find dom column
+ int domColumnIndex = -1;
+ for (Element e = cellElement; e != null; e = e
+ .getPreviousSiblingElement()) {
+ domColumnIndex++;
+ }
+
+ // Find dom row
+ int domRowIndex = -1;
+ for (Element e = cellElement.getParentElement(); e != null; e = e
+ .getPreviousSiblingElement()) {
+ domRowIndex++;
+ }
+
+ return new Cell(domRowIndex, domColumnIndex, cellElement);
+ }
+
+ double getMaxCellWidth(int colIndex) throws IllegalArgumentException {
+ double maxCellWidth = -1;
+
+ assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM.";
+
+ NodeList<TableRowElement> rows = root.getRows();
+ for (int row = 0; row < rows.getLength(); row++) {
+ TableRowElement rowElement = rows.getItem(row);
+ TableCellElement cellOriginal = rowElement.getCells().getItem(
+ colIndex);
+
+ if (cellIsPartOfSpan(cellOriginal)) {
+ continue;
+ }
+
+ /*
+ * To get the actual width of the contents, we need to get the
+ * cell content without any hardcoded height or width.
+ *
+ * But we don't want to modify the existing column, because that
+ * might trigger some unnecessary listeners and whatnot. So,
+ * instead, we make a deep clone of that cell, but without any
+ * explicit dimensions, and measure that instead.
+ */
+
+ TableCellElement cellClone = TableCellElement
+ .as((Element) cellOriginal.cloneNode(true));
+ cellClone.getStyle().clearHeight();
+ cellClone.getStyle().clearWidth();
+
+ rowElement.insertBefore(cellClone, cellOriginal);
+ double requiredWidth = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(cellClone);
+
+ if (BrowserInfo.get().isIE9()) {
+ /*
+ * IE9 does not support subpixels. Usually it is rounded
+ * down which leads to content not shown. Increase the
+ * counted required size by one just to be on the safe side.
+ */
+ requiredWidth += 1;
+ }
+
+ maxCellWidth = Math.max(requiredWidth, maxCellWidth);
+ cellClone.removeFromParent();
+ }
+
+ return maxCellWidth;
+ }
+
+ private boolean cellIsPartOfSpan(TableCellElement cell) {
+ boolean cellHasColspan = cell.getColSpan() > 1;
+ boolean cellIsHidden = Display.NONE.getCssName().equals(
+ cell.getStyle().getDisplay());
+ return cellHasColspan || cellIsHidden;
+ }
+
+ void refreshColumns(int index, int numberOfColumns) {
+ if (getRowCount() > 0) {
+ Range rowRange = Range.withLength(0, getRowCount());
+ Range colRange = Range.withLength(index, numberOfColumns);
+ refreshCells(rowRange, colRange);
+ }
+ }
+ }
+
+ private abstract class AbstractStaticRowContainer extends
+ AbstractRowContainer {
+ public AbstractStaticRowContainer(final TableSectionElement headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ for (int i = index; i < index + numberOfRows; i++) {
+ final TableRowElement tr = root.getRows().getItem(index);
+ paintRemoveRow(tr, index);
+ }
+ recalculateSectionHeight();
+ }
+
+ @Override
+ protected TableRowElement getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < root.getChildCount()) {
+ return root.getRows().getItem(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ @Override
+ public void insertRows(int index, int numberOfRows) {
+ super.insertRows(index, numberOfRows);
+ recalculateElementSizes();
+ applyHeightByRows();
+ }
+
+ @Override
+ public void removeRows(int index, int numberOfRows) {
+
+ /*
+ * While the rows in a static section are removed, the scrollbar is
+ * temporarily shrunk and then re-expanded. This leads to the fact
+ * that the scroll position is scooted up a bit. This means that we
+ * need to reset the position here.
+ *
+ * If Escalator, at some point, gets a JIT evaluation functionality,
+ * this re-setting is a strong candidate for removal.
+ */
+ double oldScrollPos = verticalScrollbar.getScrollPos();
+
+ super.removeRows(index, numberOfRows);
+ recalculateElementSizes();
+ applyHeightByRows();
+
+ verticalScrollbar.setScrollPos(oldScrollPos);
+ }
+
+ @Override
+ protected void reapplyDefaultRowHeights() {
+ if (root.getChildCount() == 0) {
+ return;
+ }
+
+ Profiler.enter("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
+
+ Element tr = root.getRows().getItem(0);
+ while (tr != null) {
+ reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight());
+ tr = tr.getNextSiblingElement();
+ }
+
+ /*
+ * Because all rows are immediately displayed in the static row
+ * containers, the section's overall height has most probably
+ * changed.
+ */
+ recalculateSectionHeight();
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ Profiler.enter("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
+
+ double newHeight = calculateTotalRowHeight();
+ if (newHeight != heightOfSection) {
+ heightOfSection = newHeight;
+ sectionHeightCalculated();
+
+ /*
+ * We need to update the scrollbar dimension at this point. If
+ * we are scrolled too far down and the static section shrinks,
+ * the body will try to render rows that don't exist during
+ * body.verifyEscalatorCount. This is because the logical row
+ * indices are calculated from the scrollbar position.
+ */
+ verticalScrollbar.setOffsetSize(heightOfEscalator
+ - header.heightOfSection - footer.heightOfSection);
+
+ body.verifyEscalatorCount();
+ }
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
+ }
+
+ /**
+ * Informs the row container that the height of its respective table
+ * section has changed.
+ * <p>
+ * These calculations might affect some layouting logic, such as the
+ * body is being offset by the footer, the footer needs to be readjusted
+ * according to its height, and so on.
+ * <p>
+ * A table section is either header, body or footer.
+ */
+ protected abstract void sectionHeightCalculated();
+
+ @Override
+ protected void refreshCells(Range logicalRowRange, Range colRange) {
+ Profiler.enter("Escalator.AbstractStaticRowContainer.refreshRows");
+
+ assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
+ logicalRowRange.length());
+
+ if (!isAttached()) {
+ return;
+ }
+
+ /*
+ * TODO [[rowheight]]: even if no rows are evaluated in the current
+ * viewport, the heights of some unrendered rows might change in a
+ * refresh. This would cause the scrollbar to be adjusted (in
+ * scrollHeight and/or scrollTop). Do we want to take this into
+ * account?
+ */
+ if (hasColumnAndRowData()) {
+ /*
+ * TODO [[rowheight]]: nudge rows down with
+ * refreshRowPositions() as needed
+ */
+ for (int row = logicalRowRange.getStart(); row < logicalRowRange
+ .getEnd(); row++) {
+ final TableRowElement tr = getTrByVisualIndex(row);
+ refreshRow(tr, row, colRange);
+ }
+ }
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.refreshRows");
+ }
+
+ @Override
+ protected void paintInsertRows(int visualIndex, int numberOfRows) {
+ paintInsertStaticRows(visualIndex, numberOfRows);
+ }
+ }
+
+ private class HeaderRowContainer extends AbstractStaticRowContainer {
+ public HeaderRowContainer(final TableSectionElement headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
+ verticalScrollbar.getElement().getStyle()
+ .setTop(heightOfSection, Unit.PX);
+ headerDeco.getStyle().setHeight(heightOfSection, Unit.PX);
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "th";
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
+ }
+ }
+
+ private class FooterRowContainer extends AbstractStaticRowContainer {
+ public FooterRowContainer(final TableSectionElement footElement) {
+ super(footElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ int vscrollHeight = (int) Math.floor(heightOfEscalator
+ - header.heightOfSection - footer.heightOfSection);
+
+ final boolean horizontalScrollbarNeeded = columnConfiguration
+ .calculateRowWidth() > widthOfEscalator;
+ if (horizontalScrollbarNeeded) {
+ vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
+ }
+
+ footerDeco.getStyle().setHeight(footer.heightOfSection, Unit.PX);
+
+ verticalScrollbar.setOffsetSize(vscrollHeight);
+ }
+ }
+
+ private class BodyRowContainer extends AbstractRowContainer {
+ /*
+ * TODO [[optimize]]: check whether a native JsArray might be faster
+ * than LinkedList
+ */
+ /**
+ * The order in which row elements are rendered visually in the browser,
+ * with the help of CSS tricks. Usually has nothing to do with the DOM
+ * order.
+ *
+ * @see #sortDomElements()
+ */
+ private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<TableRowElement>();
+
+ /**
+ * The logical index of the topmost row.
+ *
+ * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
+ * {@link #updateTopRowLogicalIndex(int)} and
+ * {@link #getTopRowLogicalIndex()} instead
+ */
+ @Deprecated
+ private int topRowLogicalIndex = 0;
+
+ private void setTopRowLogicalIndex(int topRowLogicalIndex) {
+ if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
+ Logger.getLogger("Escalator.BodyRowContainer").fine(
+ "topRowLogicalIndex: " + this.topRowLogicalIndex
+ + " -> " + topRowLogicalIndex);
+ }
+ assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative (top left cell contents: "
+ + visualRowOrder.getFirst().getCells().getItem(0)
+ .getInnerText() + ") ";
+ /*
+ * if there's a smart way of evaluating and asserting the max index,
+ * this would be a nice place to put it. I haven't found out an
+ * effective and generic solution.
+ */
+
+ this.topRowLogicalIndex = topRowLogicalIndex;
+ }
+
+ private int getTopRowLogicalIndex() {
+ return topRowLogicalIndex;
+ }
+
+ private void updateTopRowLogicalIndex(int diff) {
+ setTopRowLogicalIndex(topRowLogicalIndex + diff);
+ }
+
+ private class DeferredDomSorter {
+ private static final int SORT_DELAY_MILLIS = 50;
+
+ // as it happens, 3 frames = 50ms @ 60fps.
+ private static final int REQUIRED_FRAMES_PASSED = 3;
+
+ private final AnimationCallback frameCounter = new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ framesPassed++;
+ boolean domWasSorted = sortIfConditionsMet();
+ if (!domWasSorted) {
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(this);
+ } else {
+ waiting = false;
+ }
+ }
+ };
+
+ private int framesPassed;
+ private double startTime;
+ private AnimationHandle animationHandle;
+
+ /** <code>true</code> if a sort is scheduled */
+ public boolean waiting = false;
+
+ public void reschedule() {
+ waiting = true;
+ resetConditions();
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(frameCounter);
+ }
+
+ private boolean sortIfConditionsMet() {
+ boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
+ boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS;
+ boolean notAnimatingFlick = (scroller.currentFlickScroller == null);
+ boolean conditionsMet = enoughFramesHavePassed
+ && enoughTimeHasPassed && notAnimatingFlick;
+
+ if (conditionsMet) {
+ resetConditions();
+ sortDomElements();
+ }
+
+ return conditionsMet;
+ }
+
+ private void resetConditions() {
+ if (animationHandle != null) {
+ animationHandle.cancel();
+ animationHandle = null;
+ }
+ startTime = Duration.currentTimeMillis();
+ framesPassed = 0;
+ }
+ }
+
+ private DeferredDomSorter domSorter = new DeferredDomSorter();
+
+ public BodyRowContainer(final TableSectionElement bodyElement) {
+ super(bodyElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
+ }
+
+ public void updateEscalatorRowsOnScroll() {
+ if (visualRowOrder.isEmpty()) {
+ return;
+ }
+
+ boolean rowsWereMoved = false;
+
+ final double topRowPos = getRowTop(visualRowOrder.getFirst());
+ // TODO [[mpixscroll]]
+ final double scrollTop = tBodyScrollTop;
+ final double viewportOffset = topRowPos - scrollTop;
+
+ /*
+ * TODO [[optimize]] this if-else can most probably be refactored
+ * into a neater block of code
+ */
+
+ if (viewportOffset > 0) {
+ // there's empty room on top
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ int originalRowsToMove = (int) Math.ceil(viewportOffset
+ / getDefaultRowHeight());
+ int rowsToMove = Math.min(originalRowsToMove,
+ root.getChildCount());
+
+ final int end = root.getChildCount();
+ final int start = end - rowsToMove;
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight());
+ moveAndUpdateEscalatorRows(Range.between(start, end), 0,
+ logicalRowIndex);
+
+ setTopRowLogicalIndex(logicalRowIndex);
+
+ rowsWereMoved = true;
+ }
+
+ else if (viewportOffset + getDefaultRowHeight() <= 0) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+
+ /*
+ * the viewport has been scrolled more than the topmost visual
+ * row.
+ */
+
+ int originalRowsToMove = (int) Math.abs(viewportOffset
+ / getDefaultRowHeight());
+ int rowsToMove = Math.min(originalRowsToMove,
+ root.getChildCount());
+
+ int logicalRowIndex;
+ if (rowsToMove < root.getChildCount()) {
+ /*
+ * We scroll so little that we can just keep adding the rows
+ * below the current escalator
+ */
+ logicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ /*
+ * Since we're moving all escalator rows, we need to
+ * calculate the first logical row index from the scroll
+ * position.
+ */
+ logicalRowIndex = (int) (scrollTop / getDefaultRowHeight());
+ }
+
+ /*
+ * Since we're moving the viewport downwards, the visual index
+ * is always at the bottom. Note: Due to how
+ * moveAndUpdateEscalatorRows works, this will work out even if
+ * we move all the rows, and try to place them "at the end".
+ */
+ final int targetVisualIndex = root.getChildCount();
+
+ // make sure that we don't move rows over the data boundary
+ boolean aRowWasLeftBehind = false;
+ if (logicalRowIndex + rowsToMove > getRowCount()) {
+ /*
+ * TODO [[rowheight]]: with constant row heights, there's
+ * always exactly one row that will be moved beyond the data
+ * source, when viewport is scrolled to the end. This,
+ * however, isn't guaranteed anymore once row heights start
+ * varying.
+ */
+ rowsToMove--;
+ aRowWasLeftBehind = true;
+ }
+
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
+ targetVisualIndex, logicalRowIndex);
+
+ if (aRowWasLeftBehind) {
+ /*
+ * To keep visualRowOrder as a spatially contiguous block of
+ * rows, let's make sure that the one row we didn't move
+ * visually still stays with the pack.
+ */
+ final Range strayRow = Range.withOnly(0);
+
+ /*
+ * We cannot trust getLogicalRowIndex, because it hasn't yet
+ * been updated. But since we're leaving rows behind, it
+ * means we've scrolled to the bottom. So, instead, we
+ * simply count backwards from the end.
+ */
+ final int topLogicalIndex = getRowCount()
+ - visualRowOrder.size();
+ moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
+ }
+
+ final int naiveNewLogicalIndex = getTopRowLogicalIndex()
+ + originalRowsToMove;
+ final int maxLogicalIndex = getRowCount()
+ - visualRowOrder.size();
+ setTopRowLogicalIndex(Math.min(naiveNewLogicalIndex,
+ maxLogicalIndex));
+
+ rowsWereMoved = true;
+ }
+
+ if (rowsWereMoved) {
+ fireRowVisibilityChangeEvent();
+
+ if (scroller.touchHandlerBundle.touches == 0) {
+ /*
+ * this will never be called on touch scrolling. That is
+ * handled separately and explicitly by
+ * TouchHandlerBundle.touchEnd();
+ */
+ domSorter.reschedule();
+ }
+ }
+ }
+
+ @Override
+ protected void paintInsertRows(final int index, final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return;
+ }
+
+ /*
+ * TODO: this method should probably only add physical rows, and not
+ * populate them - let everything be populated as appropriate by the
+ * logic that follows.
+ *
+ * This also would lead to the fact that paintInsertRows wouldn't
+ * need to return anything.
+ */
+ final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, numberOfRows);
+
+ /*
+ * insertRows will always change the number of rows - update the
+ * scrollbar sizes.
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final boolean addedRowsAboveCurrentViewport = index
+ * getDefaultRowHeight() < getScrollTop();
+ final boolean addedRowsBelowCurrentViewport = index
+ * getDefaultRowHeight() > getScrollTop()
+ + calculateHeight();
+
+ if (addedRowsAboveCurrentViewport) {
+ /*
+ * We need to tweak the virtual viewport (scroll handle
+ * positions, table "scroll position" and row locations), but
+ * without re-evaluating any rows.
+ */
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final double yDelta = numberOfRows * getDefaultRowHeight();
+ adjustScrollPosIgnoreEvents(yDelta);
+ updateTopRowLogicalIndex(numberOfRows);
+ }
+
+ else if (addedRowsBelowCurrentViewport) {
+ // NOOP, we already recalculated scrollbars.
+ }
+
+ else { // some rows were added inside the current viewport
+
+ final int unupdatedLogicalStart = index + addedRows.size();
+ final int visualOffset = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ /*
+ * At this point, we have added new escalator rows, if so
+ * needed.
+ *
+ * If more rows were added than the new escalator rows can
+ * account for, we need to start to spin the escalator to update
+ * the remaining rows aswell.
+ */
+ final int rowsStillNeeded = numberOfRows - addedRows.size();
+ final Range unupdatedVisual = convertToVisual(Range.withLength(
+ unupdatedLogicalStart, rowsStillNeeded));
+ final int end = root.getChildCount();
+ final int start = end - unupdatedVisual.length();
+ final int visualTargetIndex = unupdatedLogicalStart
+ - visualOffset;
+ moveAndUpdateEscalatorRows(Range.between(start, end),
+ visualTargetIndex, unupdatedLogicalStart);
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ // move the surrounding rows to their correct places.
+ double rowTop = (unupdatedLogicalStart + (end - start))
+ * getDefaultRowHeight();
+ final ListIterator<TableRowElement> i = visualRowOrder
+ .listIterator(visualTargetIndex + (end - start));
+ while (i.hasNext()) {
+ final TableRowElement tr = i.next();
+ setRowPosition(tr, 0, rowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ rowTop += getDefaultRowHeight();
+ }
+
+ fireRowVisibilityChangeEvent();
+ sortDomElements();
+ }
+ }
+
+ /**
+ * Move escalator rows around, and make sure everything gets
+ * appropriately repositioned and repainted.
+ *
+ * @param visualSourceRange
+ * the range of rows to move to a new place
+ * @param visualTargetIndex
+ * the visual index where the rows will be placed to
+ * @param logicalTargetIndex
+ * the logical index to be assigned to the first moved row
+ * @throws IllegalArgumentException
+ * if any of <code>visualSourceRange.getStart()</code>,
+ * <code>visualTargetIndex</code> or
+ * <code>logicalTargetIndex</code> is a negative number; or
+ * if <code>visualTargetInfo</code> is greater than the
+ * number of escalator rows.
+ */
+ private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
+ final int visualTargetIndex, final int logicalTargetIndex)
+ throws IllegalArgumentException {
+
+ if (visualSourceRange.isEmpty()) {
+ return;
+ }
+
+ if (visualSourceRange.getStart() < 0) {
+ throw new IllegalArgumentException(
+ "Logical source start must be 0 or greater (was "
+ + visualSourceRange.getStart() + ")");
+ } else if (logicalTargetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Logical target must be 0 or greater");
+ } else if (visualTargetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Visual target must be 0 or greater");
+ } else if (visualTargetIndex > root.getChildCount()) {
+ throw new IllegalArgumentException(
+ "Visual target must not be greater than the number of escalator rows");
+ } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) {
+ Range logicalTargetRange = Range.withLength(logicalTargetIndex,
+ visualSourceRange.length());
+ Range availableRange = Range.withLength(0, getRowCount());
+ throw new IllegalArgumentException("Logical target leads "
+ + "to rows outside of the data range ("
+ + logicalTargetRange + " goes beyond " + availableRange
+ + ")");
+ }
+
+ /*
+ * Since we move a range into another range, the indices might move
+ * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
+ * the collection), the target range will end up being 8..9, instead
+ * of 10..11.
+ *
+ * This applies only if we move elements forward in the collection,
+ * not backward.
+ */
+ final int adjustedVisualTargetIndex;
+ if (visualSourceRange.getStart() < visualTargetIndex) {
+ adjustedVisualTargetIndex = visualTargetIndex
+ - visualSourceRange.length();
+ } else {
+ adjustedVisualTargetIndex = visualTargetIndex;
+ }
+
+ if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
+
+ /*
+ * Reorder the rows to their correct places within
+ * visualRowOrder (unless rows are moved back to their original
+ * places)
+ */
+
+ /*
+ * TODO [[optimize]]: move whichever set is smaller: the ones
+ * explicitly moved, or the others. So, with 10 escalator rows,
+ * if we are asked to move idx[0..8] to the end of the list,
+ * it's faster to just move idx[9] to the beginning.
+ */
+
+ final List<TableRowElement> removedRows = new ArrayList<TableRowElement>(
+ visualSourceRange.length());
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(visualSourceRange.getStart());
+ removedRows.add(tr);
+ }
+ visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
+ }
+
+ { // Refresh the contents of the affected rows
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
+ + visualSourceRange.length(); logicalIndex++) {
+ final TableRowElement tr = iter.next();
+ refreshRow(tr, logicalIndex);
+ }
+ }
+
+ { // Reposition the rows that were moved
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ double newRowTop = logicalTargetIndex * getDefaultRowHeight();
+
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final TableRowElement tr = iter.next();
+ setRowPosition(tr, 0, newRowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ newRowTop += getDefaultRowHeight();
+ }
+ }
+ }
+
+ /**
+ * Adjust the scroll position without having the scroll handler have any
+ * side-effects.
+ * <p>
+ * <em>Note:</em> {@link Scroller#onScroll()} <em>will</em> be
+ * triggered, but it will not do anything, with the help of
+ * {@link Escalator#internalScrollEventCalls}.
+ *
+ * @param yDelta
+ * the delta of pixels to scrolls. A positive value moves the
+ * viewport downwards, while a negative value moves the
+ * viewport upwards
+ */
+ public void adjustScrollPosIgnoreEvents(final double yDelta) {
+ if (yDelta == 0) {
+ return;
+ }
+
+ verticalScrollbar.setScrollPosByDelta(yDelta);
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final double rowTopPos = yDelta - (yDelta % getDefaultRowHeight());
+ for (final TableRowElement tr : visualRowOrder) {
+ setRowPosition(tr, 0, getRowTop(tr) + rowTopPos);
+ }
+ setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta);
+ }
+
+ /**
+ * Adds new physical escalator rows to the DOM at the given index if
+ * there's still a need for more escalator rows.
+ * <p>
+ * If Escalator already is at (or beyond) max capacity, this method does
+ * nothing to the DOM.
+ *
+ * @param index
+ * the index at which to add new escalator rows.
+ * <em>Note:</em>It is assumed that the index is both the
+ * visual index and the logical index.
+ * @param numberOfRows
+ * the number of rows to add at <code>index</code>
+ * @return a list of the added rows
+ */
+ private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded(
+ final int index, final int numberOfRows) {
+
+ final int escalatorRowsStillFit = getMaxEscalatorRowCapacity()
+ - root.getChildCount();
+ final int escalatorRowsNeeded = Math.min(numberOfRows,
+ escalatorRowsStillFit);
+
+ if (escalatorRowsNeeded > 0) {
+
+ final List<TableRowElement> addedRows = paintInsertStaticRows(
+ index, escalatorRowsNeeded);
+ visualRowOrder.addAll(index, addedRows);
+
+ /*
+ * We need to figure out the top positions for the rows we just
+ * added.
+ */
+ for (int i = 0; i < addedRows.size(); i++) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ setRowPosition(addedRows.get(i), 0, (index + i)
+ * getDefaultRowHeight());
+ }
+
+ /* Move the other rows away from above the added escalator rows */
+ for (int i = index + addedRows.size(); i < visualRowOrder
+ .size(); i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ setRowPosition(tr, 0, i * getDefaultRowHeight());
+ }
+
+ return addedRows;
+ } else {
+ return new ArrayList<TableRowElement>();
+ }
+ }
+
+ private int getMaxEscalatorRowCapacity() {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final int maxEscalatorRowCapacity = (int) Math
+ .ceil(calculateHeight() / getDefaultRowHeight()) + 1;
+
+ /*
+ * maxEscalatorRowCapacity can become negative if the headers and
+ * footers start to overlap. This is a crazy situation, but Vaadin
+ * blinks the components a lot, so it's feasible.
+ */
+ return Math.max(0, maxEscalatorRowCapacity);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return;
+ }
+
+ final Range viewportRange = getVisibleRowRange();
+ final Range removedRowsRange = Range
+ .withLength(index, numberOfRows);
+
+ final Range[] partitions = removedRowsRange
+ .partitionWith(viewportRange);
+ final Range removedAbove = partitions[0];
+ final Range removedLogicalInside = partitions[1];
+ final Range removedVisualInside = convertToVisual(removedLogicalInside);
+
+ /*
+ * TODO: extract the following if-block to a separate method. I'll
+ * leave this be inlined for now, to make linediff-based code
+ * reviewing easier. Probably will be moved in the following patch
+ * set.
+ */
+
+ /*
+ * Adjust scroll position in one of two scenarios:
+ *
+ * 1) Rows were removed above. Then we just need to adjust the
+ * scrollbar by the height of the removed rows.
+ *
+ * 2) There are no logical rows above, and at least the first (if
+ * not more) visual row is removed. Then we need to snap the scroll
+ * position to the first visible row (i.e. reset scroll position to
+ * absolute 0)
+ *
+ * The logic is optimized in such a way that the
+ * adjustScrollPosIgnoreEvents is called only once, to avoid extra
+ * reflows, and thus the code might seem a bit obscure.
+ */
+ final boolean firstVisualRowIsRemoved = !removedVisualInside
+ .isEmpty() && removedVisualInside.getStart() == 0;
+
+ if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final double yDelta = removedAbove.length()
+ * getDefaultRowHeight();
+ final double firstLogicalRowHeight = getDefaultRowHeight();
+ final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
+ .getScrollPos() - yDelta < firstLogicalRowHeight;
+
+ if (removedVisualInside.isEmpty()
+ && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
+ /*
+ * rows were removed from above the viewport, so all we need
+ * to do is to adjust the scroll position to account for the
+ * removed rows
+ */
+ adjustScrollPosIgnoreEvents(-yDelta);
+ } else if (removalScrollsToShowFirstLogicalRow) {
+ /*
+ * It seems like we've removed all rows from above, and also
+ * into the current viewport. This means we'll need to even
+ * out the scroll position to exactly 0 (i.e. adjust by the
+ * current negative scrolltop, presto!), so that it isn't
+ * aligned funnily
+ */
+ adjustScrollPosIgnoreEvents(-verticalScrollbar
+ .getScrollPos());
+ }
+ }
+
+ // ranges evaluated, let's do things.
+ if (!removedVisualInside.isEmpty()) {
+ int escalatorRowCount = bodyElem.getChildCount();
+
+ /*
+ * remember: the rows have already been subtracted from the row
+ * count at this point
+ */
+ int rowsLeft = getRowCount();
+ if (rowsLeft < escalatorRowCount) {
+ int escalatorRowsToRemove = escalatorRowCount - rowsLeft;
+ for (int i = 0; i < escalatorRowsToRemove; i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+
+ paintRemoveRow(tr, index);
+ removeRowPosition(tr);
+ }
+ escalatorRowCount -= escalatorRowsToRemove;
+
+ /*
+ * Because we're removing escalator rows, we don't have
+ * anything to scroll by. Let's make sure the viewport is
+ * scrolled to top, to render any rows possibly left above.
+ */
+ body.setBodyScrollPosition(tBodyScrollLeft, 0);
+
+ /*
+ * We might have removed some rows from the middle, so let's
+ * make sure we're not left with any holes. Also remember:
+ * visualIndex == logicalIndex applies now.
+ */
+ final int dirtyRowsStart = removedLogicalInside.getStart();
+ for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default
+ * row heights - will not work with variable row heights
+ */
+ setRowPosition(tr, 0, i * getDefaultRowHeight());
+ }
+
+ /*
+ * this is how many rows appeared into the viewport from
+ * below
+ */
+ final int rowsToUpdateDataOn = numberOfRows
+ - escalatorRowsToRemove;
+ final int start = Math.max(0, escalatorRowCount
+ - rowsToUpdateDataOn);
+ final int end = escalatorRowCount;
+ for (int i = start; i < end; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ refreshRow(tr, i);
+ }
+ }
+
+ else {
+ // No escalator rows need to be removed.
+
+ /*
+ * Two things (or a combination thereof) can happen:
+ *
+ * 1) We're scrolled to the bottom, the last rows are
+ * removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * bottommost rows, and place them at the top to be
+ * refreshed.
+ *
+ * 2) We're scrolled somewhere in the middle, arbitrary rows
+ * are removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * removed rows, and place them at the bottom to be
+ * refreshed.
+ *
+ * Since a combination can also happen, we need to handle
+ * this in a smart way, all while avoiding
+ * double-refreshing.
+ */
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final double contentBottom = getRowCount()
+ * getDefaultRowHeight();
+ final double viewportBottom = tBodyScrollTop
+ + calculateHeight();
+ if (viewportBottom <= contentBottom) {
+ /*
+ * We're in the middle of the row container, everything
+ * is added to the bottom
+ */
+ paintRemoveRowsAtMiddle(removedLogicalInside,
+ removedVisualInside, 0);
+ }
+
+ else if (removedVisualInside.contains(0)
+ && numberOfRows >= visualRowOrder.size()) {
+ /*
+ * We're removing so many rows that the viewport is
+ * pushed up more than a screenful. This means we can
+ * simply scroll up and everything will work without a
+ * sweat.
+ */
+
+ double left = horizontalScrollbar.getScrollPos();
+ double top = contentBottom - visualRowOrder.size()
+ * getDefaultRowHeight();
+ setBodyScrollPosition(left, top);
+
+ Range allEscalatorRows = Range.withLength(0,
+ visualRowOrder.size());
+ int logicalTargetIndex = getRowCount()
+ - allEscalatorRows.length();
+ moveAndUpdateEscalatorRows(allEscalatorRows, 0,
+ logicalTargetIndex);
+
+ /*
+ * Scrolling the body to the correct location will be
+ * fixed automatically. Because the amount of rows is
+ * decreased, the viewport is pushed up as the scrollbar
+ * shrinks. So no need to do anything there.
+ *
+ * TODO [[optimize]]: This might lead to a double body
+ * refresh. Needs investigation.
+ */
+ }
+
+ else if (contentBottom
+ + (numberOfRows * getDefaultRowHeight())
+ - viewportBottom < getDefaultRowHeight()) {
+ /*
+ * We're at the end of the row container, everything is
+ * added to the top.
+ */
+
+ /*
+ * FIXME [[rowheight]]: above if-clause is coded to only
+ * work with default row heights - will not work with
+ * variable row heights
+ */
+
+ paintRemoveRowsAtBottom(removedLogicalInside,
+ removedVisualInside);
+ updateTopRowLogicalIndex(-removedLogicalInside.length());
+ }
+
+ else {
+ /*
+ * We're in a combination, where we need to both scroll
+ * up AND show new rows at the bottom.
+ *
+ * Example: Scrolled down to show the second to last
+ * row. Remove two. Viewport scrolls up, revealing the
+ * row above row. The last element collapses up and into
+ * view.
+ *
+ * Reminder: this use case handles only the case when
+ * there are enough escalator rows to still render a
+ * full view. I.e. all escalator rows will _always_ be
+ * populated
+ */
+ /*-
+ * 1 1 |1| <- newly rendered
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |*|
+ * 5 5
+ *
+ * 1 1 |1| <- newly rendered
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |4|
+ * 5 5
+ */
+
+ /*
+ * STEP 1:
+ *
+ * reorganize deprecated escalator rows to bottom, but
+ * don't re-render anything yet
+ */
+ /*-
+ * 1 1 1
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |*|
+ * |4| |4| |*|
+ * 5 5 5
+ */
+ double newTop = getRowTop(visualRowOrder
+ .get(removedVisualInside.getStart()));
+ for (int i = 0; i < removedVisualInside.length(); i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ visualRowOrder.addLast(tr);
+ }
+
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, (int) newTop);
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with
+ * default row heights - will not work with variable
+ * row heights
+ */
+ newTop += getDefaultRowHeight();
+ }
+
+ /*
+ * STEP 2:
+ *
+ * manually scroll
+ */
+ /*-
+ * 1 |1| <-- newly rendered (by scrolling)
+ * |4| |4|
+ * |*| ==> |*|
+ * |*|
+ * 5 5
+ */
+ final double newScrollTop = contentBottom
+ - calculateHeight();
+ setScrollTop(newScrollTop);
+ /*
+ * Manually call the scroll handler, so we get immediate
+ * effects in the escalator.
+ */
+ scroller.onScroll();
+
+ /*
+ * Move the bottommost (n+1:th) escalator row to top,
+ * because scrolling up doesn't handle that for us
+ * automatically
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withOnly(escalatorRowCount - 1),
+ 0,
+ getLogicalRowIndex(visualRowOrder.getFirst()) - 1);
+ updateTopRowLogicalIndex(-1);
+
+ /*
+ * STEP 3:
+ *
+ * update remaining escalator rows
+ */
+ /*-
+ * |1| |1|
+ * |4| ==> |4|
+ * |*| |5| <-- newly rendered
+ *
+ * 5
+ */
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default
+ * row heights - will not work with variable row heights
+ */
+ final int rowsScrolled = (int) (Math
+ .ceil((viewportBottom - contentBottom)
+ / getDefaultRowHeight()));
+ final int start = escalatorRowCount
+ - (removedVisualInside.length() - rowsScrolled);
+ final Range visualRefreshRange = Range.between(start,
+ escalatorRowCount);
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) + start;
+ // in-place move simply re-renders the rows.
+ moveAndUpdateEscalatorRows(visualRefreshRange, start,
+ logicalTargetIndex);
+ }
+ }
+
+ fireRowVisibilityChangeEvent();
+ sortDomElements();
+ }
+
+ updateTopRowLogicalIndex(-removedAbove.length());
+
+ /*
+ * this needs to be done after the escalator has been shrunk down,
+ * or it won't work correctly (due to setScrollTop invocation)
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
+ final Range removedVisualInside, final int logicalOffset) {
+ /*-
+ * : : :
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |4|
+ * |4| |4| |6| <- newly rendered
+ * : : :
+ */
+
+ final int escalatorRowCount = visualRowOrder.size();
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getLast())
+ - (removedVisualInside.length() - 1)
+ + logicalOffset;
+ moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<TableRowElement> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getStart());
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ double rowTop = (removedLogicalInside.getStart() + logicalOffset)
+ * getDefaultRowHeight();
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount
+ - removedVisualInside.length(); i++) {
+ final TableRowElement tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ rowTop += getDefaultRowHeight();
+ }
+ }
+
+ private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
+ final Range removedVisualInside) {
+ /*-
+ * :
+ * : : |4| <- newly rendered
+ * |5| |5| |5|
+ * |6| ==> |*| ==> |7|
+ * |7| |7|
+ */
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) - removedVisualInside.length();
+ moveAndUpdateEscalatorRows(removedVisualInside, 0,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<TableRowElement> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getEnd());
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ double rowTop = removedLogicalInside.getStart()
+ * getDefaultRowHeight();
+ while (iterator.hasNext()) {
+ final TableRowElement tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ rowTop += getDefaultRowHeight();
+ }
+ }
+
+ private int getLogicalRowIndex(final Element tr) {
+ assert tr.getParentNode() == root : "The given element isn't a row element in the body";
+ int internalIndex = visualRowOrder.indexOf(tr);
+ return getTopRowLogicalIndex() + internalIndex;
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ // NOOP for body, since it doesn't make any sense.
+ }
+
+ /**
+ * Adjusts the row index and number to be relevant for the current
+ * virtual viewport.
+ * <p>
+ * It converts a logical range of rows index to the matching visual
+ * range, truncating the resulting range with the viewport.
+ * <p>
+ * <ul>
+ * <li>Escalator contains logical rows 0..100
+ * <li>Current viewport showing logical rows 20..29
+ * <li>convertToVisual([20..29]) &rarr; [0..9]
+ * <li>convertToVisual([15..24]) &rarr; [0..4]
+ * <li>convertToVisual([25..29]) &rarr; [5..9]
+ * <li>convertToVisual([26..39]) &rarr; [6..9]
+ * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([0..100]) &rarr; [0..9]
+ * </ul>
+ *
+ * @return a logical range converted to a visual range, truncated to the
+ * current viewport. The first visual row has the index 0.
+ */
+ private Range convertToVisual(final Range logicalRange) {
+ if (logicalRange.isEmpty()) {
+ return logicalRange;
+ } else if (visualRowOrder.isEmpty()) {
+ // empty range
+ return Range.withLength(0, 0);
+ }
+
+ /*
+ * TODO [[rowheight]]: these assumptions will be totally broken with
+ * variable row heights.
+ */
+ final int maxEscalatorRows = getMaxEscalatorRowCapacity();
+ final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ final Range[] partitions = logicalRange.partitionWith(Range
+ .withLength(currentTopRowIndex, maxEscalatorRows));
+ final Range insideRange = partitions[1];
+ return insideRange.offsetBy(-currentTopRowIndex);
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+
+ /**
+ * Calculates the height of the {@code <tbody>} as it should be rendered
+ * in the DOM.
+ */
+ private double calculateHeight() {
+ final int tableHeight = tableWrapper.getOffsetHeight();
+ final double footerHeight = footer.heightOfSection;
+ final double headerHeight = header.heightOfSection;
+ return tableHeight - footerHeight - headerHeight;
+ }
+
+ @Override
+ protected void refreshCells(Range logicalRowRange, Range colRange) {
+ Profiler.enter("Escalator.BodyRowContainer.refreshRows");
+
+ final Range visualRange = convertToVisual(logicalRowRange);
+
+ if (!visualRange.isEmpty()) {
+ final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+ for (int rowNumber = visualRange.getStart(); rowNumber < visualRange
+ .getEnd(); rowNumber++) {
+ refreshRow(visualRowOrder.get(rowNumber),
+ firstLogicalRowIndex + rowNumber, colRange);
+ }
+ }
+
+ Profiler.leave("Escalator.BodyRowContainer.refreshRows");
+ }
+
+ @Override
+ protected TableRowElement getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < visualRowOrder.size()) {
+ return visualRowOrder.get(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index) {
+ if (index < 0 || index >= getRowCount()) {
+ throw new IndexOutOfBoundsException("No such logical index: "
+ + index);
+ }
+ int visualIndex = index
+ - getLogicalRowIndex(visualRowOrder.getFirst());
+ if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
+ return super.getRowElement(visualIndex);
+ } else {
+ throw new IllegalStateException("Row with logical index "
+ + index + " is currently not available in the DOM");
+ }
+ }
+
+ private void setBodyScrollPosition(final double scrollLeft,
+ final double scrollTop) {
+ tBodyScrollLeft = scrollLeft;
+ tBodyScrollTop = scrollTop;
+ position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
+ }
+
+ /**
+ * Make sure that there is a correct amount of escalator rows: Add more
+ * if needed, or remove any superfluous ones.
+ * <p>
+ * This method should be called when e.g. the height of the Escalator
+ * changes.
+ * <p>
+ * <em>Note:</em> This method will make sure that the escalator rows are
+ * placed in the proper places. By default new rows are added below, but
+ * if the content is scrolled down, the rows are populated on top
+ * instead.
+ */
+ public void verifyEscalatorCount() {
+ /*
+ * This method indeed has a smell very similar to paintRemoveRows
+ * and paintInsertRows.
+ *
+ * Unfortunately, those the code can't trivially be shared, since
+ * there are some slight differences in the respective
+ * responsibilities. The "paint" methods fake the addition and
+ * removal of rows, and make sure to either push existing data out
+ * of view, or draw new data into view. Only in some special cases
+ * will the DOM element count change.
+ *
+ * This method, however, has the explicit responsibility to verify
+ * that when "something" happens, we still have the correct amount
+ * of escalator rows in the DOM, and if not, we make sure to modify
+ * that count. Only in some special cases do we need to take into
+ * account other things than simply modifying the DOM element count.
+ */
+
+ Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
+
+ if (!isAttached()) {
+ return;
+ }
+
+ final int maxEscalatorRows = getMaxEscalatorRowCapacity();
+ final int neededEscalatorRows = Math.min(maxEscalatorRows,
+ body.getRowCount());
+ final int neededEscalatorRowsDiff = neededEscalatorRows
+ - visualRowOrder.size();
+
+ if (neededEscalatorRowsDiff > 0) {
+ // needs more
+
+ /*
+ * This is a workaround for the issue where we might be scrolled
+ * to the bottom, and the widget expands beyond the content
+ * range
+ */
+
+ final int index = visualRowOrder.size();
+ final int nextLastLogicalIndex;
+ if (!visualRowOrder.isEmpty()) {
+ nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ nextLastLogicalIndex = 0;
+ }
+
+ final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
+ - neededEscalatorRowsDiff;
+ if (contentWillFit) {
+ final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, neededEscalatorRowsDiff);
+
+ /*
+ * Since fillAndPopulateEscalatorRowsIfNeeded operates on
+ * the assumption that index == visual index == logical
+ * index, we thank for the added escalator rows, but since
+ * they're painted in the wrong CSS position, we need to
+ * move them to their actual locations.
+ *
+ * Note: this is the second (see body.paintInsertRows)
+ * occasion where fillAndPopulateEscalatorRowsIfNeeded would
+ * behave "more correctly" if it only would add escalator
+ * rows to the DOM and appropriate bookkeping, and not
+ * actually populate them :/
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withLength(index, addedRows.size()), index,
+ nextLastLogicalIndex);
+ } else {
+ /*
+ * TODO [[optimize]]
+ *
+ * We're scrolled so far down that all rows can't be simply
+ * appended at the end, since we might start displaying
+ * escalator rows that don't exist. To avoid the mess that
+ * is body.paintRemoveRows, this is a dirty hack that dumbs
+ * the problem down to a more basic and already-solved
+ * problem:
+ *
+ * 1) scroll all the way up 2) add the missing escalator
+ * rows 3) scroll back to the original position.
+ *
+ * Letting the browser scroll back to our original position
+ * will automatically solve any possible overflow problems,
+ * since the browser will not allow us to scroll beyond the
+ * actual content.
+ */
+
+ final double oldScrollTop = getScrollTop();
+ setScrollTop(0);
+ scroller.onScroll();
+ fillAndPopulateEscalatorRowsIfNeeded(index,
+ neededEscalatorRowsDiff);
+ setScrollTop(oldScrollTop);
+ scroller.onScroll();
+ }
+ }
+
+ else if (neededEscalatorRowsDiff < 0) {
+ // needs less
+
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(visualRowOrder.size());
+ for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
+ final Element last = iter.previous();
+ last.removeFromParent();
+ iter.remove();
+ }
+
+ /*
+ * If we were scrolled to the bottom so that we didn't have an
+ * extra escalator row at the bottom, we'll probably end up with
+ * blank space at the bottom of the escalator, and one extra row
+ * above the header.
+ *
+ * Experimentation idea #1: calculate "scrollbottom" vs content
+ * bottom and remove one row from top, rest from bottom. This
+ * FAILED, since setHeight has already happened, thus we never
+ * will detect ourselves having been scrolled all the way to the
+ * bottom.
+ */
+
+ if (!visualRowOrder.isEmpty()) {
+ final double firstRowTop = getRowTop(visualRowOrder
+ .getFirst());
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final double firstRowMinTop = tBodyScrollTop
+ - getDefaultRowHeight();
+ if (firstRowTop < firstRowMinTop) {
+ final int newLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ moveAndUpdateEscalatorRows(Range.withOnly(0),
+ visualRowOrder.size(), newLogicalIndex);
+ }
+ }
+ }
+
+ if (neededEscalatorRowsDiff != 0) {
+ fireRowVisibilityChangeEvent();
+ }
+
+ Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
+ }
+
+ @Override
+ protected void reapplyDefaultRowHeights() {
+ if (visualRowOrder.isEmpty()) {
+ return;
+ }
+
+ /*
+ * As an intermediate step between hard-coded row heights to crazily
+ * varying row heights, Escalator will support the modification of
+ * the default row height (which is applied to all rows).
+ *
+ * This allows us to do some assumptions and simplifications for
+ * now. This code is intended to be quite short-lived, but gives
+ * insight into what needs to be done when row heights change in the
+ * body, in a general sense.
+ *
+ * TODO [[rowheight]] remove this comment once row heights may
+ * genuinely vary.
+ */
+
+ Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
+
+ /* step 1: resize and reposition rows */
+ for (int i = 0; i < visualRowOrder.size(); i++) {
+ TableRowElement tr = visualRowOrder.get(i);
+ reapplyRowHeight(tr, getDefaultRowHeight());
+
+ final int logicalIndex = getTopRowLogicalIndex() + i;
+ setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight());
+ }
+
+ /*
+ * step 2: move scrollbar so that it corresponds to its previous
+ * place
+ */
+
+ /*
+ * This ratio needs to be calculated with the scrollsize (not max
+ * scroll position) in order to align the top row with the new
+ * scroll position.
+ */
+ double scrollRatio = verticalScrollbar.getScrollPos()
+ / verticalScrollbar.getScrollSize();
+ scroller.recalculateScrollbarsForVirtualViewport();
+ verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
+ * getRowCount() * scrollRatio));
+ setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
+ verticalScrollbar.getScrollPos());
+ scroller.onScroll();
+
+ /* step 3: make sure we have the correct amount of escalator rows. */
+ verifyEscalatorCount();
+
+ /*
+ * TODO [[rowheight]] This simply doesn't work with variable rows
+ * heights.
+ */
+ int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight());
+ setTopRowLogicalIndex(logicalLogical);
+
+ Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
+ }
+
+ /**
+ * Sorts the rows in the DOM to correspond to the visual order.
+ *
+ * @see #visualRowOrder
+ */
+ private void sortDomElements() {
+ final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
+ Profiler.enter(profilingName);
+
+ /*
+ * Focus is lost from an element if that DOM element is (or any of
+ * its parents are) removed from the document. Therefore, we sort
+ * everything around that row instead.
+ */
+ final TableRowElement focusedRow = getEscalatorRowWithFocus();
+
+ if (focusedRow != null) {
+ assert focusedRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
+ assert visualRowOrder.contains(focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder.";
+ }
+
+ /*
+ * Two cases handled simultaneously:
+ *
+ * 1) No focus on rows. We iterate visualRowOrder backwards, and
+ * take the respective element in the DOM, and place it as the first
+ * child in the body element. Then we take the next-to-last from
+ * visualRowOrder, and put that first, pushing the previous row as
+ * the second child. And so on...
+ *
+ * 2) Focus on some row within Escalator body. Again, we iterate
+ * visualRowOrder backwards. This time, we use the focused row as a
+ * pivot: Instead of placing rows from the bottom of visualRowOrder
+ * and placing it first, we place it underneath the focused row.
+ * Once we hit the focused row, we don't move it (to not reset
+ * focus) but change sorting mode. After that, we place all rows as
+ * the first child.
+ */
+
+ /*
+ * If we have a focused row, start in the mode where we put
+ * everything underneath that row. Otherwise, all rows are placed as
+ * first child.
+ */
+ boolean insertFirst = (focusedRow == null);
+
+ final ListIterator<TableRowElement> i = visualRowOrder
+ .listIterator(visualRowOrder.size());
+ while (i.hasPrevious()) {
+ TableRowElement tr = i.previous();
+
+ if (tr == focusedRow) {
+ insertFirst = true;
+ } else if (insertFirst) {
+ root.insertFirst(tr);
+ } else {
+ root.insertAfter(tr, focusedRow);
+ }
+ }
+
+ Profiler.leave(profilingName);
+ }
+
+ /**
+ * Get the escalator row that has focus.
+ *
+ * @return The escalator row that contains a focused DOM element, or
+ * <code>null</code> if focus is outside of a body row.
+ */
+ private TableRowElement getEscalatorRowWithFocus() {
+ TableRowElement rowContainingFocus = null;
+
+ final Element focusedElement = WidgetUtil.getFocusedElement();
+
+ if (focusedElement != null && root.isOrHasChild(focusedElement)) {
+ Element e = focusedElement;
+
+ while (e != null && e != root) {
+ /*
+ * You never know if there's several tables embedded in a
+ * cell... We'll take the deepest one.
+ */
+ if (TableRowElement.is(e)) {
+ rowContainingFocus = TableRowElement.as(e);
+ }
+ e = e.getParentElement();
+ }
+ }
+
+ return rowContainingFocus;
+ }
+
+ @Override
+ public Cell getCell(Element element) {
+ Cell cell = super.getCell(element);
+ if (cell == null) {
+ return null;
+ }
+
+ // Convert DOM coordinates to logical coordinates for rows
+ Element rowElement = cell.getElement().getParentElement();
+ return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
+ cell.getElement());
+ }
+ }
+
+ private class ColumnConfigurationImpl implements ColumnConfiguration {
+ public class Column {
+ private static final int DEFAULT_COLUMN_WIDTH_PX = 100;
+
+ private double definedWidth = -1;
+ private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
+ private boolean measuringRequested = false;
+
+ /**
+ * If a column has been created (either via insertRow or
+ * insertColumn), it will be given an arbitrary width, and only then
+ * a width will be defined.
+ */
+ private boolean widthHasBeenFinalized = false;
+
+ public void setWidth(double px) {
+ definedWidth = px;
+
+ if (px < 0) {
+ if (isAttached()) {
+ calculateWidth();
+ } else {
+ /*
+ * the column's width is calculated at Escalator.onLoad
+ * via measureIfNeeded!
+ */
+ measuringRequested = true;
+ }
+ } else {
+ calculatedWidth = px;
+ }
+ }
+
+ public double getDefinedWidth() {
+ return definedWidth;
+ }
+
+ /**
+ * Returns the actual width in the DOM.
+ *
+ * @return the width in pixels in the DOM. Returns -1 if the column
+ * needs measuring, but has not been yet measured
+ */
+ public double getCalculatedWidth() {
+ /*
+ * This might return an untrue value (e.g. during init/onload),
+ * since we haven't had a proper chance to actually calculate
+ * widths yet.
+ *
+ * This is fixed during Escalator.onLoad, by the call to
+ * "measureIfNeeded", which fixes "everything".
+ */
+ if (!measuringRequested) {
+ return calculatedWidth;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Checks if the column needs measuring, and then measures it.
+ * <p>
+ * Called by {@link Escalator#onLoad()}.
+ */
+ public boolean measureAndSetWidthIfNeeded() {
+ assert isAttached() : "Column.measureIfNeeded() was called even though Escalator was not attached!";
+
+ if (measuringRequested) {
+ measuringRequested = false;
+ setWidth(definedWidth);
+ return true;
+ }
+ return false;
+ }
+
+ private void calculateWidth() {
+ calculatedWidth = getMaxCellWidth(columns.indexOf(this));
+ }
+
+ public void widthIsFinalized() {
+ columnAutoWidthAssignScheduler.cancel();
+ widthHasBeenFinalized = true;
+ }
+
+ public boolean isWidthFinalized() {
+ return widthHasBeenFinalized;
+ }
+ }
+
+ private final List<Column> columns = new ArrayList<Column>();
+ private int frozenColumns = 0;
+
+ /**
+ * A cached array of all the calculated column widths.
+ *
+ * @see #getCalculatedColumnWidths()
+ */
+ private double[] widthsArray = null;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeColumns(final int index, final int numberOfColumns) {
+ // Validate
+ assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
+
+ // Move the horizontal scrollbar to the left, if removed columns are
+ // to the left of the viewport
+ removeColumnsAdjustScrollbar(index, numberOfColumns);
+
+ // Remove from DOM
+ header.paintRemoveColumns(index, numberOfColumns);
+ body.paintRemoveColumns(index, numberOfColumns);
+ footer.paintRemoveColumns(index, numberOfColumns);
+
+ // Remove from bookkeeping
+ flyweightRow.removeCells(index, numberOfColumns);
+ columns.subList(index, index + numberOfColumns).clear();
+
+ // Adjust frozen columns
+ if (index < getFrozenColumnCount()) {
+ if (index + numberOfColumns < frozenColumns) {
+ /*
+ * Last removed column was frozen, meaning that all removed
+ * columns were frozen. Just decrement the number of frozen
+ * columns accordingly.
+ */
+ frozenColumns -= numberOfColumns;
+ } else {
+ /*
+ * If last removed column was not frozen, we have removed
+ * columns beyond the frozen range, so all remaining frozen
+ * columns are to the left of the removed columns.
+ */
+ frozenColumns = index;
+ }
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ body.verifyEscalatorCount();
+
+ if (getColumnConfiguration().getColumnCount() > 0) {
+ reapplyRowWidths(header);
+ reapplyRowWidths(body);
+ reapplyRowWidths(footer);
+ }
+
+ /*
+ * Colspans make any kind of automatic clever content re-rendering
+ * impossible: As soon as anything has colspans, removing one might
+ * reveal further colspans, modifying the DOM structure once again,
+ * ending in a cascade of updates. Because we don't know how the
+ * data is updated.
+ *
+ * So, instead, we don't do anything. The client code is responsible
+ * for re-rendering the content (if so desired). Everything Just
+ * Works (TM) if colspans aren't used.
+ */
+ }
+
+ private void reapplyRowWidths(AbstractRowContainer container) {
+ if (container.getRowCount() > 0) {
+ container.reapplyRowWidths();
+ }
+ }
+
+ private void removeColumnsAdjustScrollbar(int index, int numberOfColumns) {
+ if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar
+ .getScrollSize()) {
+ return;
+ }
+
+ double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth(Range
+ .between(0, index));
+ double widthOfColumnsToRemove = getCalculatedColumnsWidth(Range
+ .withLength(index, numberOfColumns));
+
+ double scrollLeft = horizontalScrollbar.getScrollPos();
+
+ if (scrollLeft <= leftPosOfFirstColumnToRemove) {
+ /*
+ * viewport is scrolled to the left of the first removed column,
+ * so there's no need to adjust anything
+ */
+ return;
+ }
+
+ double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove,
+ scrollLeft - widthOfColumnsToRemove);
+ horizontalScrollbar.setScrollPos(adjustedScrollLeft);
+ }
+
+ /**
+ * Calculate the width of a row, as the sum of columns' widths.
+ *
+ * @return the width of a row, in pixels
+ */
+ public double calculateRowWidth() {
+ return getCalculatedColumnsWidth(Range.between(0, getColumnCount()));
+ }
+
+ private void assertArgumentsAreValidAndWithinRange(final int index,
+ final int numberOfColumns) {
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns can't be less than 1 (was "
+ + numberOfColumns + ")");
+ }
+
+ if (index < 0 || index + numberOfColumns > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "column range (" + index + ".."
+ + (index + numberOfColumns)
+ + ") was outside of the current "
+ + "number of columns (" + getColumnCount() + ")");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows when this
+ * method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertColumns(final int index, final int numberOfColumns) {
+ // Validate
+ if (index < 0 || index > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given index(" + index
+ + ") was outside of the current number of columns (0.."
+ + getColumnCount() + ")");
+ }
+
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns must be 1 or greater (was "
+ + numberOfColumns);
+ }
+
+ // Add to bookkeeping
+ flyweightRow.addCells(index, numberOfColumns);
+ for (int i = 0; i < numberOfColumns; i++) {
+ columns.add(index, new Column());
+ }
+
+ // Adjust frozen columns
+ boolean frozen = index < frozenColumns;
+ if (frozen) {
+ frozenColumns += numberOfColumns;
+ }
+
+ // this needs to be before the scrollbar adjustment.
+ boolean scrollbarWasNeeded = horizontalScrollbar.getOffsetSize() < horizontalScrollbar
+ .getScrollSize();
+ scroller.recalculateScrollbarsForVirtualViewport();
+ boolean scrollbarIsNowNeeded = horizontalScrollbar.getOffsetSize() < horizontalScrollbar
+ .getScrollSize();
+ if (!scrollbarWasNeeded && scrollbarIsNowNeeded) {
+ body.verifyEscalatorCount();
+ }
+
+ // Add to DOM
+ header.paintInsertColumns(index, numberOfColumns, frozen);
+ body.paintInsertColumns(index, numberOfColumns, frozen);
+ footer.paintInsertColumns(index, numberOfColumns, frozen);
+
+ // fix autowidth
+ if (header.getRowCount() > 0 || body.getRowCount() > 0
+ || footer.getRowCount() > 0) {
+ for (int col = index; col < index + numberOfColumns; col++) {
+ getColumnConfiguration().setColumnWidth(col, -1);
+ columnConfiguration.columns.get(col).widthIsFinalized();
+ }
+ }
+
+ // Adjust scrollbar
+ double pixelsToInsertedColumn = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, index));
+ final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
+
+ if (columnsWereAddedToTheLeftOfViewport) {
+ double insertedColumnsWidth = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(index,
+ numberOfColumns));
+ horizontalScrollbar.setScrollPos(scroller.lastScrollLeft
+ + insertedColumnsWidth);
+ }
+
+ /*
+ * Colspans make any kind of automatic clever content re-rendering
+ * impossible: As soon as anything has colspans, adding one might
+ * affect surrounding colspans, modifying the DOM structure once
+ * again, ending in a cascade of updates. Because we don't know how
+ * the data is updated.
+ *
+ * So, instead, we don't do anything. The client code is responsible
+ * for re-rendering the content (if so desired). Everything Just
+ * Works (TM) if colspans aren't used.
+ */
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ @Override
+ public void setFrozenColumnCount(int count)
+ throws IllegalArgumentException {
+ if (count < 0 || count > getColumnCount()) {
+ throw new IllegalArgumentException(
+ "count must be between 0 and the current number of columns ("
+ + getColumnCount() + ")");
+ }
+ int oldCount = frozenColumns;
+ if (count == oldCount) {
+ return;
+ }
+
+ frozenColumns = count;
+
+ if (hasSomethingInDom()) {
+ // Are we freezing or unfreezing?
+ boolean frozen = count > oldCount;
+
+ int firstAffectedCol;
+ int firstUnaffectedCol;
+
+ if (frozen) {
+ firstAffectedCol = oldCount;
+ firstUnaffectedCol = count;
+ } else {
+ firstAffectedCol = count;
+ firstUnaffectedCol = oldCount;
+ }
+
+ for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
+ header.setColumnFrozen(col, frozen);
+ body.setColumnFrozen(col, frozen);
+ footer.setColumnFrozen(col, frozen);
+ }
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ @Override
+ public int getFrozenColumnCount() {
+ return frozenColumns;
+ }
+
+ @Override
+ public void setColumnWidth(int index, double px)
+ throws IllegalArgumentException {
+ checkValidColumnIndex(index);
+
+ columns.get(index).setWidth(px);
+ columns.get(index).widthIsFinalized();
+ widthsArray = null;
+
+ /*
+ * TODO [[optimize]]: only modify the elements that are actually
+ * modified.
+ */
+ header.reapplyColumnWidths();
+ body.reapplyColumnWidths();
+ footer.reapplyColumnWidths();
+ recalculateElementSizes();
+ }
+
+ private void checkValidColumnIndex(int index)
+ throws IllegalArgumentException {
+ if (!Range.withLength(0, getColumnCount()).contains(index)) {
+ throw new IllegalArgumentException("The given column index ("
+ + index + ") does not exist");
+ }
+ }
+
+ @Override
+ public double getColumnWidth(int index) throws IllegalArgumentException {
+ checkValidColumnIndex(index);
+ return columns.get(index).getDefinedWidth();
+ }
+
+ @Override
+ public double getColumnWidthActual(int index) {
+ return columns.get(index).getCalculatedWidth();
+ }
+
+ private double getMaxCellWidth(int colIndex)
+ throws IllegalArgumentException {
+ double headerWidth = header.getMaxCellWidth(colIndex);
+ double bodyWidth = body.getMaxCellWidth(colIndex);
+ double footerWidth = footer.getMaxCellWidth(colIndex);
+
+ double maxWidth = Math.max(headerWidth,
+ Math.max(bodyWidth, footerWidth));
+ assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible.";
+ return maxWidth;
+ }
+
+ /**
+ * Calculates the width of the columns in a given range.
+ *
+ * @param columns
+ * the columns to calculate
+ * @return the total width of the columns in the given
+ * <code>columns</code>
+ */
+ double getCalculatedColumnsWidth(final Range columns) {
+ /*
+ * This is an assert instead of an exception, since this is an
+ * internal method.
+ */
+ assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range "
+ + "was outside of current column range (i.e.: "
+ + Range.between(0, getColumnCount())
+ + ", but was given :"
+ + columns;
+
+ double sum = 0;
+ for (int i = columns.getStart(); i < columns.getEnd(); i++) {
+ double columnWidthActual = getColumnWidthActual(i);
+ sum += columnWidthActual;
+ }
+ return sum;
+ }
+
+ double[] getCalculatedColumnWidths() {
+ if (widthsArray == null || widthsArray.length != getColumnCount()) {
+ widthsArray = new double[getColumnCount()];
+ for (int i = 0; i < columns.size(); i++) {
+ widthsArray[i] = columns.get(i).getCalculatedWidth();
+ }
+ }
+ return widthsArray;
+ }
+
+ @Override
+ public void refreshColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns must be 1 or greater (was "
+ + numberOfColumns + ")");
+ }
+
+ if (index < 0 || index + numberOfColumns > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "column range (" + index + ".."
+ + (index + numberOfColumns)
+ + ") was outside of the current number of columns ("
+ + getColumnCount() + ")");
+ }
+
+ header.refreshColumns(index, numberOfColumns);
+ body.refreshColumns(index, numberOfColumns);
+ footer.refreshColumns(index, numberOfColumns);
+ }
+ }
+
+ // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
+ /**
+ * The solution to
+ * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;30</code>
+ * .
+ * <p>
+ * This constant is placed in the Escalator class, instead of an inner
+ * class, since even mathematical expressions aren't allowed in non-static
+ * inner classes for constants.
+ */
+ private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
+ /**
+ * The solution to
+ * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;40</code>
+ * .
+ * <p>
+ * This constant is placed in the Escalator class, instead of an inner
+ * class, since even mathematical expressions aren't allowed in non-static
+ * inner classes for constants.
+ */
+ private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
+
+ private static final String DEFAULT_WIDTH = "500.0px";
+ private static final String DEFAULT_HEIGHT = "400.0px";
+
+ private FlyweightRow flyweightRow = new FlyweightRow();
+
+ /** The {@code <thead/>} tag. */
+ private final TableSectionElement headElem = TableSectionElement.as(DOM
+ .createTHead());
+ /** The {@code <tbody/>} tag. */
+ private final TableSectionElement bodyElem = TableSectionElement.as(DOM
+ .createTBody());
+ /** The {@code <tfoot/>} tag. */
+ private final TableSectionElement footElem = TableSectionElement.as(DOM
+ .createTFoot());
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private double tBodyScrollTop = 0;
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private double tBodyScrollLeft = 0;
+
+ private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
+ private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
+
+ private final HeaderRowContainer header = new HeaderRowContainer(headElem);
+ private final BodyRowContainer body = new BodyRowContainer(bodyElem);
+ private final FooterRowContainer footer = new FooterRowContainer(footElem);
+
+ private final Scroller scroller = new Scroller();
+
+ private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
+ private final DivElement tableWrapper;
+
+ private final DivElement horizontalScrollbarDeco = DivElement.as(DOM
+ .createDiv());
+ private final DivElement headerDeco = DivElement.as(DOM.createDiv());
+ private final DivElement footerDeco = DivElement.as(DOM.createDiv());
+
+ private PositionFunction position;
+
+ /** The cached width of the escalator, in pixels. */
+ private double widthOfEscalator = 0;
+ /** The cached height of the escalator, in pixels. */
+ private double heightOfEscalator = 0;
+
+ /** The height of Escalator in terms of body rows. */
+ private double heightByRows = 10.0d;
+
+ /** The height of Escalator, as defined by {@link #setHeight(String)} */
+ private String heightByCss = "";
+
+ private HeightMode heightMode = HeightMode.CSS;
+
+ private boolean layoutIsScheduled = false;
+ private ScheduledCommand layoutCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ recalculateElementSizes();
+ layoutIsScheduled = false;
+ }
+ };
+
+ private final ColumnAutoWidthAssignScheduler columnAutoWidthAssignScheduler = new ColumnAutoWidthAssignScheduler();
+
+ /**
+ * Creates a new Escalator widget instance.
+ */
+ public Escalator() {
+
+ detectAndApplyPositionFunction();
+ getLogger().info(
+ "Using " + position.getClass().getSimpleName()
+ + " for position");
+
+ final Element root = DOM.createDiv();
+ setElement(root);
+
+ ScrollHandler scrollHandler = new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ scroller.onScroll();
+ fireEvent(new ScrollEvent());
+ }
+ };
+
+ root.appendChild(verticalScrollbar.getElement());
+ verticalScrollbar.addScrollHandler(scrollHandler);
+ verticalScrollbar.setScrollbarThickness(WidgetUtil
+ .getNativeScrollbarSize());
+
+ root.appendChild(horizontalScrollbar.getElement());
+ horizontalScrollbar.addScrollHandler(scrollHandler);
+ horizontalScrollbar.setScrollbarThickness(WidgetUtil
+ .getNativeScrollbarSize());
+ horizontalScrollbar
+ .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
+ @Override
+ public void visibilityChanged(
+ ScrollbarBundle.VisibilityChangeEvent event) {
+ /*
+ * We either lost or gained a scrollbar. In any case, we
+ * need to change the height, if it's defined by rows.
+ */
+ applyHeightByRows();
+ }
+ });
+
+ tableWrapper = DivElement.as(DOM.createDiv());
+
+ root.appendChild(tableWrapper);
+
+ final Element table = DOM.createTable();
+ tableWrapper.appendChild(table);
+
+ table.appendChild(headElem);
+ table.appendChild(bodyElem);
+ table.appendChild(footElem);
+
+ Style hCornerStyle = headerDeco.getStyle();
+ hCornerStyle.setWidth(WidgetUtil.getNativeScrollbarSize(), Unit.PX);
+ hCornerStyle.setDisplay(Display.NONE);
+ root.appendChild(headerDeco);
+
+ Style fCornerStyle = footerDeco.getStyle();
+ fCornerStyle.setWidth(WidgetUtil.getNativeScrollbarSize(), Unit.PX);
+ fCornerStyle.setDisplay(Display.NONE);
+ root.appendChild(footerDeco);
+
+ Style hWrapperStyle = horizontalScrollbarDeco.getStyle();
+ hWrapperStyle.setDisplay(Display.NONE);
+ hWrapperStyle.setHeight(WidgetUtil.getNativeScrollbarSize(), Unit.PX);
+ root.appendChild(horizontalScrollbarDeco);
+
+ setStylePrimaryName("v-escalator");
+
+ // init default dimensions
+ setHeight(null);
+ setWidth(null);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ header.autodetectRowHeightLater();
+ body.autodetectRowHeightLater();
+ footer.autodetectRowHeightLater();
+
+ header.paintInsertRows(0, header.getRowCount());
+ footer.paintInsertRows(0, footer.getRowCount());
+ recalculateElementSizes();
+ /*
+ * Note: There's no need to explicitly insert rows into the body.
+ *
+ * recalculateElementSizes will recalculate the height of the body. This
+ * has the side-effect that as the body's size grows bigger (i.e. from 0
+ * to its actual height), more escalator rows are populated. Those
+ * escalator rows are then immediately rendered. This, in effect, is the
+ * same thing as inserting those rows.
+ *
+ * In fact, having an extra paintInsertRows here would lead to duplicate
+ * rows.
+ */
+
+ boolean columnsChanged = false;
+ for (ColumnConfigurationImpl.Column column : columnConfiguration.columns) {
+ boolean columnChanged = column.measureAndSetWidthIfNeeded();
+ if (columnChanged) {
+ columnsChanged = true;
+ }
+ }
+ if (columnsChanged) {
+ header.reapplyColumnWidths();
+ body.reapplyColumnWidths();
+ footer.reapplyColumnWidths();
+ }
+
+ scroller.attachScrollListener(verticalScrollbar.getElement());
+ scroller.attachScrollListener(horizontalScrollbar.getElement());
+ scroller.attachMousewheelListener(getElement());
+ scroller.attachTouchListeners(getElement());
+ }
+
+ @Override
+ protected void onUnload() {
+
+ scroller.detachScrollListener(verticalScrollbar.getElement());
+ scroller.detachScrollListener(horizontalScrollbar.getElement());
+ scroller.detachMousewheelListener(getElement());
+ scroller.detachTouchListeners(getElement());
+
+ /*
+ * We can call paintRemoveRows here, because static ranges are simple to
+ * remove.
+ */
+ header.paintRemoveRows(0, header.getRowCount());
+ footer.paintRemoveRows(0, footer.getRowCount());
+
+ /*
+ * We can't call body.paintRemoveRows since it relies on rowCount to be
+ * updated correctly. Since it isn't, we'll simply and brutally rip out
+ * the DOM elements (in an elegant way, of course).
+ */
+ int rowsToRemove = bodyElem.getChildCount();
+ for (int i = 0; i < rowsToRemove; i++) {
+ int index = rowsToRemove - i - 1;
+ TableRowElement tr = bodyElem.getRows().getItem(index);
+ body.paintRemoveRow(tr, index);
+ body.removeRowPosition(tr);
+ }
+ body.visualRowOrder.clear();
+ body.setTopRowLogicalIndex(0);
+
+ super.onUnload();
+ }
+
+ private void detectAndApplyPositionFunction() {
+ /*
+ * firefox has a bug in its translate operation, showing white space
+ * when adjusting the scrollbar in BodyRowContainer.paintInsertRows
+ */
+ if (Window.Navigator.getUserAgent().contains("Firefox")) {
+ position = new AbsolutePosition();
+ return;
+ }
+
+ final Style docStyle = Document.get().getBody().getStyle();
+ if (hasProperty(docStyle, "transform")) {
+ if (hasProperty(docStyle, "transformStyle")) {
+ position = new Translate3DPosition();
+ } else {
+ position = new TranslatePosition();
+ }
+ } else if (hasProperty(docStyle, "webkitTransform")) {
+ position = new WebkitTranslate3DPosition();
+ } else {
+ position = new AbsolutePosition();
+ }
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ private static native boolean hasProperty(Style style, String name)
+ /*-{
+ return style[name] !== undefined;
+ }-*/;
+
+ /**
+ * Check whether there are both columns and any row data (for either
+ * headers, body or footer).
+ *
+ * @return <code>true</code> iff header, body or footer has rows && there
+ * are columns
+ */
+ private boolean hasColumnAndRowData() {
+ return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
+ .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
+ }
+
+ /**
+ * Check whether there are any cells in the DOM.
+ *
+ * @return <code>true</code> iff header, body or footer has any child
+ * elements
+ */
+ private boolean hasSomethingInDom() {
+ return headElem.hasChildNodes() || bodyElem.hasChildNodes()
+ || footElem.hasChildNodes();
+ }
+
+ /**
+ * Returns the row container for the header in this Escalator.
+ *
+ * @return the header. Never <code>null</code>
+ */
+ public RowContainer getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the row container for the body in this Escalator.
+ *
+ * @return the body. Never <code>null</code>
+ */
+ public RowContainer getBody() {
+ return body;
+ }
+
+ /**
+ * Returns the row container for the footer in this Escalator.
+ *
+ * @return the footer. Never <code>null</code>
+ */
+ public RowContainer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Returns the configuration object for the columns in this Escalator.
+ *
+ * @return the configuration object for the columns in this Escalator. Never
+ * <code>null</code>
+ */
+ public ColumnConfiguration getColumnConfiguration() {
+ return columnConfiguration;
+ }
+
+ @Override
+ public void setWidth(final String width) {
+ if (width != null && !width.isEmpty()) {
+ super.setWidth(width);
+ } else {
+ super.setWidth(DEFAULT_WIDTH);
+ }
+
+ recalculateElementSizes();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If Escalator is currently not in {@link HeightMode#CSS}, the given value
+ * is remembered, and applied once the mode is applied.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(String height) {
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented
+ */
+
+ if (height != null && !height.isEmpty()) {
+ heightByCss = height;
+ } else {
+ heightByCss = DEFAULT_HEIGHT;
+ }
+
+ if (getHeightMode() == HeightMode.CSS) {
+ setHeightInternal(height);
+ }
+ }
+
+ private void setHeightInternal(final String height) {
+ final int escalatorRowsBefore = body.visualRowOrder.size();
+
+ if (height != null && !height.isEmpty()) {
+ super.setHeight(height);
+ } else {
+ super.setHeight(DEFAULT_HEIGHT);
+ }
+
+ recalculateElementSizes();
+
+ if (escalatorRowsBefore != body.visualRowOrder.size()) {
+ fireRowVisibilityChangeEvent();
+ }
+ }
+
+ /**
+ * Returns the vertical scroll offset. Note that this is not necessarily the
+ * same as the {@code scrollTop} attribute in the DOM.
+ *
+ * @return the logical vertical scroll offset
+ */
+ public double getScrollTop() {
+ return verticalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the vertical scroll offset. Note that this will not necessarily
+ * become the same as the {@code scrollTop} attribute in the DOM.
+ *
+ * @param scrollTop
+ * the number of pixels to scroll vertically
+ */
+ public void setScrollTop(final double scrollTop) {
+ verticalScrollbar.setScrollPos(scrollTop);
+ }
+
+ /**
+ * Returns the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the {@code scrollLeft} attribute in the DOM.
+ *
+ * @return the logical horizontal scroll offset
+ */
+ public double getScrollLeft() {
+ return horizontalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the logical horizontal scroll offset. Note that will not necessarily
+ * become the same as the {@code scrollLeft} attribute in the DOM.
+ *
+ * @param scrollLeft
+ * the number of pixels to scroll horizontally
+ */
+ public void setScrollLeft(final double scrollLeft) {
+ horizontalScrollbar.setScrollPos(scrollLeft);
+ }
+
+ /**
+ * Scrolls the body horizontally so that the column at the given index is
+ * visible and there is at least {@code padding} pixels in the direction of
+ * the given scroll destination.
+ *
+ * @param columnIndex
+ * the index of the column to scroll to
+ * @param destination
+ * where the column should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to column and
+ * the viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code columnIndex} is not a valid index for an existing
+ * column
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero, or if the indicated column is frozen
+ */
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ if (destination == ScrollDestination.MIDDLE && padding != 0) {
+ throw new IllegalArgumentException(
+ "You cannot have a padding with a MIDDLE destination");
+ }
+ verifyValidColumnIndex(columnIndex);
+
+ if (columnIndex < columnConfiguration.frozenColumns) {
+ throw new IllegalArgumentException("The given column index "
+ + columnIndex + " is frozen.");
+ }
+
+ scroller.scrollToColumn(columnIndex, destination, padding);
+ }
+
+ private void verifyValidColumnIndex(final int columnIndex)
+ throws IndexOutOfBoundsException {
+ if (columnIndex < 0
+ || columnIndex >= columnConfiguration.getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given column index "
+ + columnIndex + " does not exist.");
+ }
+ }
+
+ /**
+ * Scrolls the body vertically so that the row at the given index is visible
+ * and there is at least {@literal padding} pixels to the given scroll
+ * destination.
+ *
+ * @param rowIndex
+ * the index of the logical row to scroll to
+ * @param destination
+ * where the row should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to row and the
+ * viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code rowIndex} is not a valid index for an existing row
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero
+ */
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ if (destination == ScrollDestination.MIDDLE && padding != 0) {
+ throw new IllegalArgumentException(
+ "You cannot have a padding with a MIDDLE destination");
+ }
+ verifyValidRowIndex(rowIndex);
+
+ scroller.scrollToRow(rowIndex, destination, padding);
+ }
+
+ private void verifyValidRowIndex(final int rowIndex) {
+ if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
+ throw new IndexOutOfBoundsException("The given row index "
+ + rowIndex + " does not exist.");
+ }
+ }
+
+ /**
+ * Recalculates the dimensions for all elements that require manual
+ * calculations. Also updates the dimension caches.
+ * <p>
+ * <em>Note:</em> This method has the <strong>side-effect</strong>
+ * automatically makes sure that an appropriate amount of escalator rows are
+ * present. So, if the body area grows, more <strong>escalator rows might be
+ * inserted</strong>. Conversely, if the body area shrinks,
+ * <strong>escalator rows might be removed</strong>.
+ */
+ private void recalculateElementSizes() {
+ if (!isAttached()) {
+ return;
+ }
+
+ Profiler.enter("Escalator.recalculateElementSizes");
+ widthOfEscalator = Math.max(0, WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(getElement()));
+ heightOfEscalator = Math.max(0, WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(getElement()));
+
+ header.recalculateSectionHeight();
+ body.recalculateSectionHeight();
+ footer.recalculateSectionHeight();
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ body.verifyEscalatorCount();
+ Profiler.leave("Escalator.recalculateElementSizes");
+ }
+
+ /**
+ * Snap deltas of x and y to the major four axes (up, down, left, right)
+ * with a threshold of a number of degrees from those axes.
+ *
+ * @param deltaX
+ * the delta in the x axis
+ * @param deltaY
+ * the delta in the y axis
+ * @param thresholdRatio
+ * the threshold in ratio (0..1) between x and y for when to snap
+ * @return a two-element array: <code>[snappedX, snappedY]</code>
+ */
+ private static double[] snapDeltas(final double deltaX,
+ final double deltaY, final double thresholdRatio) {
+
+ final double[] array = new double[2];
+ if (deltaX != 0 && deltaY != 0) {
+ final double aDeltaX = Math.abs(deltaX);
+ final double aDeltaY = Math.abs(deltaY);
+ final double yRatio = aDeltaY / aDeltaX;
+ final double xRatio = aDeltaX / aDeltaY;
+
+ array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
+ array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
+ } else {
+ array[0] = deltaX;
+ array[1] = deltaY;
+ }
+
+ return array;
+ }
+
+ /**
+ * Adds an event handler that gets notified when the range of visible rows
+ * changes e.g. because of scrolling or row resizing.
+ *
+ * @param rowVisibilityChangeHandler
+ * the event handler
+ * @return a handler registration for the added handler
+ */
+ public HandlerRegistration addRowVisibilityChangeHandler(
+ RowVisibilityChangeHandler rowVisibilityChangeHandler) {
+ return addHandler(rowVisibilityChangeHandler,
+ RowVisibilityChangeEvent.TYPE);
+ }
+
+ private void fireRowVisibilityChangeEvent() {
+ if (!body.visualRowOrder.isEmpty()) {
+ int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder
+ .getFirst());
+ int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder
+ .getLast()) + 1;
+
+ int visibleRowCount = visibleRangeEnd - visibleRangeStart;
+ fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
+ visibleRowCount));
+ } else {
+ fireEvent(new RowVisibilityChangeEvent(0, 0));
+ }
+ }
+
+ /**
+ * Gets the range of currently visible rows.
+ *
+ * @return range of visible rows
+ */
+ public Range getVisibleRowRange() {
+ if (!body.visualRowOrder.isEmpty()) {
+ return Range.withLength(
+ body.getLogicalRowIndex(body.visualRowOrder.getFirst()),
+ body.visualRowOrder.size());
+ } else {
+ return Range.withLength(0, 0);
+ }
+ }
+
+ /**
+ * Returns the widget from a cell node or <code>null</code> if there is no
+ * widget in the cell
+ *
+ * @param cellNode
+ * The cell node
+ */
+ static Widget getWidgetFromCell(Node cellNode) {
+ Node possibleWidgetNode = cellNode.getFirstChild();
+ if (possibleWidgetNode != null
+ && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
+ @SuppressWarnings("deprecation")
+ com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
+ .cast();
+ Widget w = WidgetUtil.findWidget(castElement, null);
+
+ // Ensure findWidget did not traverse past the cell element in the
+ // DOM hierarchy
+ if (cellNode.isOrHasChild(w.getElement())) {
+ return w;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+
+ verticalScrollbar.setStylePrimaryName(style);
+ horizontalScrollbar.setStylePrimaryName(style);
+
+ UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
+ UIObject.setStylePrimaryName(headerDeco, style + "-header-deco");
+ UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco");
+ UIObject.setStylePrimaryName(horizontalScrollbarDeco, style
+ + "-horizontal-scrollbar-deco");
+
+ header.setStylePrimaryName(style);
+ body.setStylePrimaryName(style);
+ footer.setStylePrimaryName(style);
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Escalator's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Escalator is currently not in {@link HeightMode#ROW}, the given value
+ * is remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * the number of rows that should be visible in Escalator's body
+ * @throws IllegalArgumentException
+ * if {@code rows} is &leq; 0,
+ * {@link Double#isInifinite(double) infinite} or
+ * {@link Double#isNaN(double) NaN}.
+ * @see #setHeightMode(HeightMode)
+ */
+ public void setHeightByRows(double rows) throws IllegalArgumentException {
+ if (rows <= 0) {
+ throw new IllegalArgumentException(
+ "The number of rows must be a positive number.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "The number of rows must be finite.");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("The number must not be NaN.");
+ }
+
+ heightByRows = rows;
+ applyHeightByRows();
+ }
+
+ /**
+ * Gets the amount of rows in Escalator's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * By default, it is 10.
+ *
+ * @return the amount of rows that are being shown in Escalator's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return heightByRows;
+ }
+
+ /**
+ * Reapplies the row-based height of the Grid, if Grid currently should
+ * define its height that way.
+ */
+ private void applyHeightByRows() {
+ if (heightMode != HeightMode.ROW) {
+ return;
+ }
+
+ double headerHeight = header.heightOfSection;
+ double footerHeight = footer.heightOfSection;
+ double bodyHeight = body.getDefaultRowHeight() * heightByRows;
+ double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar
+ .getScrollbarThickness() : 0;
+
+ double totalHeight = headerHeight + bodyHeight + scrollbar
+ + footerHeight;
+ setHeightInternal(totalHeight + "px");
+ }
+
+ /**
+ * Defines the mode in which the Escalator widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Escalator will respect the values
+ * given via {@link #setHeight(String)}, and behave as a traditional Widget.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Escalator will make sure that the
+ * {@link #getBody() body} will display as many rows as
+ * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are
+ * inserted or removed, the widget will resize itself to still display the
+ * required amount of rows in its body. It also takes the horizontal
+ * scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Escalator should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ if (heightMode != this.heightMode) {
+ this.heightMode = heightMode;
+
+ switch (this.heightMode) {
+ case CSS:
+ setHeight(heightByCss);
+ break;
+ case ROW:
+ setHeightByRows(heightByRows);
+ break;
+ default:
+ throw new IllegalStateException("Unimplemented feature "
+ + "- unknown HeightMode: " + this.heightMode);
+ }
+ }
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Escalator is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return heightMode;
+ }
+
+ /**
+ * Returns the {@link RowContainer} which contains the element.
+ *
+ * @param element
+ * the element to check for
+ * @return the container the element is in or <code>null</code> if element
+ * is not present in any container.
+ */
+ public RowContainer findRowContainer(Element element) {
+ if (getHeader().getElement() != element
+ && getHeader().getElement().isOrHasChild(element)) {
+ return getHeader();
+ } else if (getBody().getElement() != element
+ && getBody().getElement().isOrHasChild(element)) {
+ return getBody();
+ } else if (getFooter().getElement() != element
+ && getFooter().getElement().isOrHasChild(element)) {
+ return getFooter();
+ }
+ return null;
+ }
+
+ /**
+ * Sets whether a scroll direction is locked or not.
+ * <p>
+ * If a direction is locked, the escalator will refuse to scroll in that
+ * direction.
+ *
+ * @param direction
+ * the orientation of the scroll to set the lock status
+ * @param locked
+ * <code>true</code> to lock, <code>false</code> to unlock
+ */
+ public void setScrollLocked(ScrollbarBundle.Direction direction,
+ boolean locked) {
+ switch (direction) {
+ case HORIZONTAL:
+ horizontalScrollbar.setLocked(locked);
+ break;
+ case VERTICAL:
+ verticalScrollbar.setLocked(locked);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unexpected value: "
+ + direction);
+ }
+ }
+
+ /**
+ * Checks whether or not an direction is locked for scrolling.
+ *
+ * @param direction
+ * the direction of the scroll of which to check the lock status
+ * @return <code>true</code> iff the direction is locked
+ */
+ public boolean isScrollLocked(ScrollbarBundle.Direction direction) {
+ switch (direction) {
+ case HORIZONTAL:
+ return horizontalScrollbar.isLocked();
+ case VERTICAL:
+ return verticalScrollbar.isLocked();
+ default:
+ throw new UnsupportedOperationException("Unexpected value: "
+ + direction);
+ }
+ }
+
+ /**
+ * Adds a scroll handler to this escalator
+ *
+ * @param handler
+ * the scroll handler to add
+ * @return a handler registration for the registered scroll handler
+ */
+ public HandlerRegistration addScrollHandler(ScrollHandler handler) {
+ return addHandler(handler, ScrollEvent.TYPE);
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return body.domSorter.waiting
+ || columnAutoWidthAssignScheduler.isScheduled
+ || verticalScrollbar.isWorkPending()
+ || horizontalScrollbar.isWorkPending();
+ }
+
+ @Override
+ public void onResize() {
+ if (isAttached() && !layoutIsScheduled) {
+ layoutIsScheduled = true;
+ Scheduler.get().scheduleDeferred(layoutCommand);
+ }
+ }
+
+ /**
+ * Gets the maximum number of body rows that can be visible on the screen at
+ * once.
+ *
+ * @return the maximum capacity
+ */
+ public int getMaxVisibleRowCount() {
+ return body.getMaxEscalatorRowCapacity();
+ }
+
+ /**
+ * Gets the escalator's inner width. This is the entire width in pixels,
+ * without the vertical scrollbar.
+ *
+ * @return escalator's inner width
+ */
+ public double getInnerWidth() {
+ return WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(tableWrapper);
+ }
+
+ /**
+ * Resets all cached pixel sizes and reads new values from the DOM. This
+ * methods should be used e.g. when styles affecting the dimensions of
+ * elements in this escalator have been changed.
+ */
+ public void resetSizesFromDom() {
+ header.autodetectRowHeightNow();
+ body.autodetectRowHeightNow();
+ footer.autodetectRowHeightNow();
+
+ for (int i = 0; i < columnConfiguration.getColumnCount(); i++) {
+ columnConfiguration.setColumnWidth(i,
+ columnConfiguration.getColumnWidth(i));
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java
new file mode 100644
index 0000000000..d6dfdee3b3
--- /dev/null
+++ b/client/src/com/vaadin/client/widgets/Grid.java
@@ -0,0 +1,5867 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.client.widgets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.Touch;
+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.KeyEvent;
+import com.google.gwt.event.dom.client.MouseEvent;
+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.touch.client.Point;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.HasEnabled;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.ResizeComposite;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.ObjectRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.renderers.WidgetRenderer;
+import com.vaadin.client.ui.SubPartAware;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.escalator.Row;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
+import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
+import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.CellStyleGenerator;
+import com.vaadin.client.widget.grid.DataAvailableEvent;
+import com.vaadin.client.widget.grid.DataAvailableHandler;
+import com.vaadin.client.widget.grid.EditorHandler;
+import com.vaadin.client.widget.grid.EditorHandler.EditorRequest;
+import com.vaadin.client.widget.grid.EditorHandler.EditorRequest.RequestCallback;
+import com.vaadin.client.widget.grid.EventCellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.grid.RowReference;
+import com.vaadin.client.widget.grid.RowStyleGenerator;
+import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler;
+import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler;
+import com.vaadin.client.widget.grid.events.BodyClickHandler;
+import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyDownHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyPressHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyUpHandler;
+import com.vaadin.client.widget.grid.events.FooterClickHandler;
+import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyDownHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyPressHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyUpHandler;
+import com.vaadin.client.widget.grid.events.GridClickEvent;
+import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
+import com.vaadin.client.widget.grid.events.GridKeyDownEvent;
+import com.vaadin.client.widget.grid.events.GridKeyPressEvent;
+import com.vaadin.client.widget.grid.events.GridKeyUpEvent;
+import com.vaadin.client.widget.grid.events.HeaderClickHandler;
+import com.vaadin.client.widget.grid.events.HeaderDoubleClickHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyDownHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyPressHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyUpHandler;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+import com.vaadin.client.widget.grid.events.SelectAllEvent;
+import com.vaadin.client.widget.grid.events.SelectAllHandler;
+import com.vaadin.client.widget.grid.selection.HasSelectionHandlers;
+import com.vaadin.client.widget.grid.selection.SelectionEvent;
+import com.vaadin.client.widget.grid.selection.SelectionHandler;
+import com.vaadin.client.widget.grid.selection.SelectionModel;
+import com.vaadin.client.widget.grid.selection.SelectionModel.Multi;
+import com.vaadin.client.widget.grid.selection.SelectionModelMulti;
+import com.vaadin.client.widget.grid.selection.SelectionModelNone;
+import com.vaadin.client.widget.grid.selection.SelectionModelSingle;
+import com.vaadin.client.widget.grid.sort.Sort;
+import com.vaadin.client.widget.grid.sort.SortEvent;
+import com.vaadin.client.widget.grid.sort.SortHandler;
+import com.vaadin.client.widget.grid.sort.SortOrder;
+import com.vaadin.client.widgets.Escalator.AbstractRowContainer;
+import com.vaadin.client.widgets.Grid.Editor.State;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * A data grid view that supports columns and lazy loading of data rows from a
+ * data source.
+ *
+ * <h1>Columns</h1>
+ * <p>
+ * Each column in Grid is represented by a {@link Column}. Each
+ * {@code GridColumn} has a custom implementation for
+ * {@link Column#getValue(Object)} that gets the row object as an argument, and
+ * returns the value for that particular column, extracted from the row object.
+ * <p>
+ * Each column also has a Renderer. Its function is to take the value that is
+ * given by the {@code GridColumn} and display it to the user. A simple column
+ * might have a {@link com.vaadin.client.renderers.TextRenderer TextRenderer}
+ * that simply takes in a {@code String} and displays it as the cell's content.
+ * A more complex renderer might be
+ * {@link com.vaadin.client.renderers.ProgressBarRenderer ProgressBarRenderer}
+ * that takes in a floating point number, and displays a progress bar instead,
+ * based on the given number.
+ * <p>
+ * <em>See:</em> {@link #addColumn(Column)}, {@link #addColumn(Column, int)} and
+ * {@link #addColumns(Column...)}. <em>Also</em>
+ * {@link Column#setRenderer(Renderer)}.
+ *
+ * <h1>Data Sources</h1>
+ * <p>
+ * Grid gets its data from a {@link DataSource}, providing row objects to Grid
+ * from a user-defined endpoint. It can be either a local in-memory data source
+ * (e.g. {@link com.vaadin.client.widget.grid.datasources.ListDataSource
+ * ListDataSource}) or even a remote one, retrieving data from e.g. a REST API
+ * (see {@link com.vaadin.client.data.AbstractRemoteDataSource
+ * AbstractRemoteDataSource}).
+ *
+ *
+ * @param <T>
+ * The row type of the grid. The row type is the POJO type from where
+ * the data is retrieved into the column cells.
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Grid<T> extends ResizeComposite implements
+ HasSelectionHandlers<T>, SubPartAware, DeferredWorker, HasWidgets,
+ HasEnabled {
+
+ /**
+ * Enum describing different sections of Grid.
+ */
+ public enum Section {
+ HEADER, BODY, FOOTER
+ }
+
+ /**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+ protected abstract static class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>> {
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ *
+ */
+ static class StaticCell {
+
+ private Object content = null;
+
+ private int colspan = 1;
+
+ private StaticSection<?> section;
+
+ private GridStaticCellType type = GridStaticCellType.TEXT;
+
+ private String styleName = null;
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ this.content = text;
+ this.type = GridStaticCellType.TEXT;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type " + type);
+ }
+ return (String) content;
+ }
+
+ protected StaticSection<?> getSection() {
+ assert section != null;
+ return section;
+ }
+
+ protected void setSection(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ /**
+ * Returns the amount of columns the cell spans. By default is 1.
+ *
+ * @return The amount of columns the cell spans.
+ */
+ public int getColspan() {
+ return colspan;
+ }
+
+ /**
+ * Sets the amount of columns the cell spans. Must be more or equal
+ * to 1. By default is 1.
+ *
+ * @param colspan
+ * the colspan to set
+ */
+ public void setColspan(int colspan) {
+ if (colspan < 1) {
+ throw new IllegalArgumentException(
+ "Colspan cannot be less than 1");
+ }
+
+ this.colspan = colspan;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the html inside the cell.
+ *
+ * @throws IllegalStateException
+ * if trying to retrive HTML from a cell with a type
+ * other than {@link GridStaticCellType#HTML}.
+ * @return the html content of the cell.
+ */
+ public String getHtml() {
+ if (type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type " + type);
+ }
+ return (String) content;
+ }
+
+ /**
+ * Sets the content of the cell to the provided html. All previous
+ * content is discarded and the cell type is set to
+ * {@link GridStaticCellType#HTML}.
+ *
+ * @param html
+ * The html content of the cell
+ */
+ public void setHtml(String html) {
+ this.content = html;
+ this.type = GridStaticCellType.HTML;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the widget in the cell.
+ *
+ * @throws IllegalStateException
+ * if the cell is not {@link GridStaticCellType#WIDGET}
+ *
+ * @return the widget in the cell
+ */
+ public Widget getWidget() {
+ if (type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Widget from a cell with type " + type);
+ }
+ return (Widget) content;
+ }
+
+ /**
+ * Set widget as the content of the cell. The type of the cell
+ * becomes {@link GridStaticCellType#WIDGET}. All previous content
+ * is discarded.
+ *
+ * @param widget
+ * The widget to add to the cell. Should not be
+ * previously attached anywhere (widget.getParent ==
+ * null).
+ */
+ public void setWidget(Widget widget) {
+ this.content = widget;
+ this.type = GridStaticCellType.WIDGET;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the type of the cell.
+ *
+ * @return the type of content the cell contains.
+ */
+ public GridStaticCellType getType() {
+ return type;
+ }
+
+ /**
+ * Returns the custom style name for this cell.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * Sets a custom style name for this cell.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ section.requestSectionRefresh();
+
+ }
+
+ }
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ abstract static class StaticRow<CELLTYPE extends StaticCell> {
+
+ private Map<Column<?, ?>, CELLTYPE> cells = new HashMap<Column<?, ?>, CELLTYPE>();
+
+ private StaticSection<?> section;
+
+ /**
+ * Map from set of spanned columns to cell meta data.
+ */
+ private Map<Set<Column<?, ?>>, CELLTYPE> cellGroups = new HashMap<Set<Column<?, ?>>, CELLTYPE>();
+
+ /**
+ * A custom style name for the row or null if none is set.
+ */
+ private String styleName = null;
+
+ /**
+ * Returns the cell on given GridColumn. If the column is merged
+ * returned cell is the cell for the whole group.
+ *
+ * @param column
+ * the column in grid
+ * @return the cell on given column, merged cell for merged columns,
+ * null if not found
+ */
+ public CELLTYPE getCell(Column<?, ?> column) {
+ Set<Column<?, ?>> cellGroup = getCellGroupForColumn(column);
+ if (cellGroup != null) {
+ return cellGroups.get(cellGroup);
+ }
+ return cells.get(column);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param columns
+ * the columns which header should be merged
+ * @return the remaining visible cell after the merge, or the cell
+ * on first column if all are hidden
+ */
+ public CELLTYPE join(Column<?, ?>... columns) {
+ if (columns.length <= 1) {
+ throw new IllegalArgumentException(
+ "You can't merge less than 2 columns together.");
+ }
+
+ HashSet<Column<?, ?>> columnGroup = new HashSet<Column<?, ?>>();
+ for (Column<?, ?> column : columns) {
+ if (!cells.containsKey(column)) {
+ throw new IllegalArgumentException(
+ "Given column does not exists on row " + column);
+ } else if (getCellGroupForColumn(column) != null) {
+ throw new IllegalStateException(
+ "Column is already in a group.");
+ }
+ columnGroup.add(column);
+ }
+
+ CELLTYPE joinedCell = createCell();
+ cellGroups.put(columnGroup, joinedCell);
+ joinedCell.setSection(getSection());
+
+ calculateColspans();
+
+ return joinedCell;
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge, or the first
+ * cell if all columns are hidden
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ if (cells.length <= 1) {
+ throw new IllegalArgumentException(
+ "You can't merge less than 2 cells together.");
+ }
+
+ Column<?, ?>[] columns = new Column<?, ?>[cells.length];
+
+ int j = 0;
+ for (Column<?, ?> column : this.cells.keySet()) {
+ CELLTYPE cell = this.cells.get(column);
+ if (!this.cells.containsValue(cells[j])) {
+ throw new IllegalArgumentException(
+ "Given cell does not exists on row");
+ } else if (cell.equals(cells[j])) {
+ columns[j++] = column;
+ if (j == cells.length) {
+ break;
+ }
+ }
+ }
+
+ return join(columns);
+ }
+
+ private Set<Column<?, ?>> getCellGroupForColumn(Column<?, ?> column) {
+ for (Set<Column<?, ?>> group : cellGroups.keySet()) {
+ if (group.contains(column)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ void calculateColspans() {
+
+ // Reset all cells
+ for (CELLTYPE cell : this.cells.values()) {
+ cell.setColspan(1);
+ }
+
+ List<Column<?, ?>> columnOrder = new ArrayList<Column<?, ?>>(
+ section.grid.getColumns());
+ // Set colspan for grouped cells
+ for (Set<Column<?, ?>> group : cellGroups.keySet()) {
+ if (!checkCellGroupAndOrder(columnOrder, group)) {
+ cellGroups.get(group).setColspan(1);
+ } else {
+ int colSpan = group.size();
+ cellGroups.get(group).setColspan(colSpan);
+ }
+ }
+
+ }
+
+ private boolean checkCellGroupAndOrder(
+ List<Column<?, ?>> columnOrder, Set<Column<?, ?>> cellGroup) {
+ if (!columnOrder.containsAll(cellGroup)) {
+ return false;
+ }
+
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!cellGroup.contains(columnOrder.get(i))) {
+ continue;
+ }
+
+ for (int j = 1; j < cellGroup.size(); ++j) {
+ if (!cellGroup.contains(columnOrder.get(i + j))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ protected void addCell(Column<?, ?> column) {
+ CELLTYPE cell = createCell();
+ cell.setSection(getSection());
+ cells.put(column, cell);
+ }
+
+ protected void removeCell(Column<?, ?> column) {
+ cells.remove(column);
+ }
+
+ protected abstract CELLTYPE createCell();
+
+ protected StaticSection<?> getSection() {
+ return section;
+ }
+
+ protected void setSection(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ /**
+ * Returns the custom style name for this row.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * Sets a custom style name for this row.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ section.requestSectionRefresh();
+ }
+ }
+
+ private Grid<?> grid;
+
+ private List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ private boolean visible = true;
+
+ /**
+ * Creates and returns a new instance of the row type.
+ *
+ * @return the created row
+ */
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that this section should be re-rendered.
+ * <p>
+ * <b>Note</b> that re-render means calling update() on each cell,
+ * preAttach()/postAttach()/preDetach()/postDetach() is not called as
+ * the cells are not removed from the DOM.
+ */
+ protected abstract void requestSectionRefresh();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ requestSectionRefresh();
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Inserts a new row at the given position. Shifts the row currently at
+ * that position and any subsequent rows down (adds one to their
+ * indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(int)
+ * @see #removeRow(StaticRow)
+ */
+ public ROWTYPE addRowAt(int index) {
+ ROWTYPE row = createRow();
+ row.setSection(this);
+ for (int i = 0; i < getGrid().getColumnCount(); ++i) {
+ row.addCell(grid.getColumn(i));
+ }
+ rows.add(index, row);
+
+ requestSectionRefresh();
+ return row;
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ * @see #appendRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(int)
+ * @see #removeRow(StaticRow)
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ * @see #prependRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(int)
+ * @see #removeRow(StaticRow)
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(StaticRow)
+ */
+ public void removeRow(int index) {
+ rows.remove(index);
+ requestSectionRefresh();
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(int)
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Returns the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ * @return the row with the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE getRow(int index) {
+ try {
+ return rows.get(index);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Row with index " + index
+ + " does not exist");
+ }
+ }
+
+ /**
+ * Returns the number of rows in this section.
+ *
+ * @return the number of rows
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected List<ROWTYPE> getRows() {
+ return rows;
+ }
+
+ protected int getVisibleRowCount() {
+ return isVisible() ? getRowCount() : 0;
+ }
+
+ protected void addColumn(Column<?, ?> column) {
+ for (ROWTYPE row : rows) {
+ row.addCell(column);
+ }
+ }
+
+ protected void removeColumn(Column<?, ?> column) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(column);
+ }
+ }
+
+ protected void setGrid(Grid<?> grid) {
+ this.grid = grid;
+ }
+
+ protected Grid<?> getGrid() {
+ assert grid != null;
+ return grid;
+ }
+ }
+
+ /**
+ * Represents the header section of a Grid. A header consists of a single
+ * header row containing a header cell for each column. Each cell has a
+ * simple textual caption.
+ */
+ protected static class Header extends StaticSection<HeaderRow> {
+ private HeaderRow defaultRow;
+
+ private boolean markAsDirty = false;
+
+ @Override
+ public void removeRow(int index) {
+ HeaderRow removedRow = getRow(index);
+ super.removeRow(index);
+ if (removedRow == defaultRow) {
+ setDefaultRow(null);
+ }
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special
+ * header row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+ if (row != null && !getRows().contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the container");
+ }
+ if (defaultRow != null) {
+ defaultRow.setDefault(false);
+ }
+ if (row != null) {
+ row.setDefault(true);
+ }
+ defaultRow = row;
+ requestSectionRefresh();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow();
+ }
+
+ @Override
+ protected void requestSectionRefresh() {
+ markAsDirty = true;
+
+ /*
+ * Defer the refresh so if we multiple times call refreshSection()
+ * (for example when updating cell values) we only get one actual
+ * refresh in the end.
+ */
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (markAsDirty) {
+ markAsDirty = false;
+ getGrid().refreshHeader();
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the events consumed by the header
+ *
+ * @return a collection of BrowserEvents
+ */
+ public Collection<String> getConsumedEvents() {
+ return Arrays.asList(BrowserEvents.TOUCHSTART,
+ BrowserEvents.TOUCHMOVE, BrowserEvents.TOUCHEND,
+ BrowserEvents.TOUCHCANCEL, BrowserEvents.CLICK);
+ }
+ }
+
+ /**
+ * A single row in a grid header section.
+ *
+ */
+ public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
+
+ private boolean isDefault = false;
+
+ protected void setDefault(boolean isDefault) {
+ this.isDefault = isDefault;
+ }
+
+ public boolean isDefault() {
+ return isDefault;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell();
+ }
+ }
+
+ /**
+ * A single cell in a grid header row. Has a textual caption.
+ *
+ */
+ public static class HeaderCell extends StaticSection.StaticCell {
+ }
+
+ /**
+ * Represents the footer section of a Grid. The footer is always empty.
+ */
+ protected static class Footer extends StaticSection<FooterRow> {
+ private boolean markAsDirty = false;
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow();
+ }
+
+ @Override
+ protected void requestSectionRefresh() {
+ markAsDirty = true;
+
+ /*
+ * Defer the refresh so if we multiple times call refreshSection()
+ * (for example when updating cell values) we only get one actual
+ * refresh in the end.
+ */
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (markAsDirty) {
+ markAsDirty = false;
+ getGrid().refreshFooter();
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * A single cell in a grid Footer row. Has a textual caption.
+ *
+ */
+ public static class FooterCell extends StaticSection.StaticCell {
+ }
+
+ /**
+ * A single row in a grid Footer section.
+ *
+ */
+ public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell();
+ }
+ }
+
+ /**
+ * An editor UI for Grid rows. A single Grid row at a time can be opened for
+ * editing.
+ */
+ protected static class Editor<T> {
+
+ public static final int KEYCODE_SHOW = KeyCodes.KEY_ENTER;
+ public static final int KEYCODE_HIDE = KeyCodes.KEY_ESCAPE;
+
+ protected enum State {
+ INACTIVE, ACTIVATING, BINDING, ACTIVE, SAVING
+ }
+
+ private Grid<T> grid;
+ private EditorHandler<T> handler;
+
+ private DivElement editorOverlay = DivElement.as(DOM.createDiv());
+
+ private Map<Column<?, T>, Widget> columnToWidget = new HashMap<Column<?, T>, Widget>();
+
+ private boolean enabled = false;
+ private State state = State.INACTIVE;
+ private int rowIndex = -1;
+ private String styleName = null;
+
+ private HandlerRegistration scrollHandler;
+
+ private Button saveButton;
+ private Button cancelButton;
+
+ private static final int SAVE_TIMEOUT_MS = 5000;
+ private final Timer saveTimeout = new Timer() {
+ @Override
+ public void run() {
+ getLogger().warning(
+ "Editor save action is taking longer than expected ("
+ + SAVE_TIMEOUT_MS + "ms). Does your "
+ + EditorHandler.class.getSimpleName()
+ + " remember to call success() or fail()?");
+ }
+ };
+
+ private final RequestCallback<T> saveRequestCallback = new RequestCallback<T>() {
+ @Override
+ public void onSuccess(EditorRequest<T> request) {
+ if (state == State.SAVING) {
+ cleanup();
+ cancel();
+ }
+ }
+
+ @Override
+ public void onError(EditorRequest<T> request) {
+ if (state == State.SAVING) {
+ cleanup();
+
+ // TODO probably not the most correct thing to do...
+ getLogger().warning(
+ "An error occurred when trying to save the "
+ + "modified row");
+ }
+ }
+
+ private void cleanup() {
+ state = State.ACTIVE;
+ setButtonsEnabled(true);
+ saveTimeout.cancel();
+ }
+ };
+
+ private static final int BIND_TIMEOUT_MS = 5000;
+ private final Timer bindTimeout = new Timer() {
+ @Override
+ public void run() {
+ getLogger().warning(
+ "Editor bind action is taking longer than expected ("
+ + BIND_TIMEOUT_MS + "ms). Does your "
+ + EditorHandler.class.getSimpleName()
+ + " remember to call success() or fail()?");
+ }
+ };
+ private final RequestCallback<T> bindRequestCallback = new RequestCallback<T>() {
+ @Override
+ public void onSuccess(EditorRequest<T> request) {
+ if (state == State.BINDING) {
+ state = State.ACTIVE;
+ bindTimeout.cancel();
+
+ showOverlay(grid.getEscalator().getBody()
+ .getRowElement(request.getRowIndex()));
+ }
+ }
+
+ @Override
+ public void onError(EditorRequest<T> request) {
+ if (state == State.BINDING) {
+ state = State.INACTIVE;
+ bindTimeout.cancel();
+
+ // TODO show something in the DOM as well?
+ getLogger().warning(
+ "An error occurred while trying to show the "
+ + "Grid editor");
+ grid.getEscalator().setScrollLocked(Direction.VERTICAL,
+ false);
+ }
+ }
+ };
+
+ public int getRow() {
+ return rowIndex;
+ }
+
+ /**
+ * Opens the editor over the row with the given index.
+ *
+ * @param rowIndex
+ * the index of the row to be edited
+ *
+ * @throws IllegalStateException
+ * if this editor is not enabled
+ * @throws IllegalStateException
+ * if this editor is already in edit mode
+ */
+ public void editRow(int rowIndex) {
+ if (!enabled) {
+ throw new IllegalStateException(
+ "Cannot edit row: editor is not enabled");
+ }
+ if (state != State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot edit row: editor already in edit mode");
+ }
+
+ this.rowIndex = rowIndex;
+
+ state = State.ACTIVATING;
+
+ if (grid.getEscalator().getVisibleRowRange().contains(rowIndex)) {
+ show();
+ } else {
+ grid.scrollToRow(rowIndex, ScrollDestination.MIDDLE);
+ }
+ }
+
+ /**
+ * Cancels the currently active edit and hides the editor. Any changes
+ * that are not {@link #save() saved} are lost.
+ *
+ * @throws IllegalStateException
+ * if this editor is not enabled
+ * @throws IllegalStateException
+ * if this editor is not in edit mode
+ */
+ public void cancel() {
+ if (!enabled) {
+ throw new IllegalStateException(
+ "Cannot cancel edit: editor is not enabled");
+ }
+ if (state == State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot cancel edit: editor is not in edit mode");
+ }
+ hideOverlay();
+ grid.getEscalator().setScrollLocked(Direction.VERTICAL, false);
+
+ EditorRequest<T> request = new EditorRequest<T>(grid, rowIndex,
+ null);
+ handler.cancel(request);
+ state = State.INACTIVE;
+ }
+
+ /**
+ * Saves any unsaved changes to the data source and hides the editor.
+ *
+ * @throws IllegalStateException
+ * if this editor is not enabled
+ * @throws IllegalStateException
+ * if this editor is not in edit mode
+ */
+ public void save() {
+ if (!enabled) {
+ throw new IllegalStateException(
+ "Cannot save: editor is not enabled");
+ }
+ if (state != State.ACTIVE) {
+ throw new IllegalStateException(
+ "Cannot save: editor is not in edit mode");
+ }
+
+ state = State.SAVING;
+ setButtonsEnabled(false);
+ saveTimeout.schedule(SAVE_TIMEOUT_MS);
+ EditorRequest<T> request = new EditorRequest<T>(grid, rowIndex,
+ saveRequestCallback);
+ handler.save(request);
+ }
+
+ /**
+ * Returns the handler responsible for binding data and editor widgets
+ * to this editor.
+ *
+ * @return the editor handler or null if not set
+ */
+ public EditorHandler<T> getHandler() {
+ return handler;
+ }
+
+ /**
+ * Sets the handler responsible for binding data and editor widgets to
+ * this editor.
+ *
+ * @param rowHandler
+ * the new editor handler
+ *
+ * @throws IllegalStateException
+ * if this editor is currently in edit mode
+ */
+ public void setHandler(EditorHandler<T> rowHandler) {
+ if (state != State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot set EditorHandler: editor is currently in edit mode");
+ }
+ handler = rowHandler;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Sets the enabled state of this editor.
+ *
+ * @param enabled
+ * true if enabled, false otherwise
+ *
+ * @throws IllegalStateException
+ * if in edit mode and trying to disable
+ * @throws IllegalStateException
+ * if the editor handler is not set
+ */
+ public void setEnabled(boolean enabled) {
+ if (enabled == false && state != State.INACTIVE) {
+ throw new IllegalStateException(
+ "Cannot disable: editor is in edit mode");
+ } else if (enabled == true && getHandler() == null) {
+ throw new IllegalStateException(
+ "Cannot enable: EditorHandler not set");
+ }
+ this.enabled = enabled;
+ }
+
+ protected void show() {
+ if (state == State.ACTIVATING) {
+ state = State.BINDING;
+ bindTimeout.schedule(BIND_TIMEOUT_MS);
+ EditorRequest<T> request = new EditorRequest<T>(grid, rowIndex,
+ bindRequestCallback);
+ handler.bind(request);
+ grid.getEscalator().setScrollLocked(Direction.VERTICAL, true);
+ }
+ }
+
+ protected void setGrid(final Grid<T> grid) {
+ assert grid != null : "Grid cannot be null";
+ assert this.grid == null : "Can only attach editor to Grid once";
+
+ this.grid = grid;
+
+ grid.addDataAvailableHandler(new DataAvailableHandler() {
+ @Override
+ public void onDataAvailable(DataAvailableEvent event) {
+ if (event.getAvailableRows().contains(rowIndex)) {
+ show();
+ }
+ }
+ });
+ }
+
+ protected State getState() {
+ return state;
+ }
+
+ protected void setState(State state) {
+ this.state = state;
+ }
+
+ /**
+ * Returns the editor widget associated with the given column. If the
+ * editor is not active, returns null.
+ *
+ * @param column
+ * the column
+ * @return the widget if the editor is open, null otherwise
+ */
+ protected Widget getWidget(Column<?, T> column) {
+ return columnToWidget.get(column);
+ }
+
+ /**
+ * Opens the editor overlay over the given table row.
+ *
+ * @param tr
+ * the row to be edited
+ */
+ protected void showOverlay(TableRowElement tr) {
+
+ DivElement tableWrapper = DivElement.as(tr.getParentElement()
+ .getParentElement().getParentElement());
+
+ AbstractRowContainer body = (AbstractRowContainer) grid
+ .getEscalator().getBody();
+
+ double rowTop = body.getRowTop(tr);
+ int bodyTop = body.getElement().getAbsoluteTop();
+ int wrapperTop = tableWrapper.getAbsoluteTop();
+
+ double width = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(tr);
+ double height = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(tr);
+ setBounds(editorOverlay, tr.getOffsetLeft(), rowTop + bodyTop
+ - wrapperTop, width, height);
+
+ updateHorizontalScrollPosition();
+
+ scrollHandler = grid.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ updateHorizontalScrollPosition();
+ }
+ });
+
+ tableWrapper.appendChild(editorOverlay);
+
+ for (int i = 0; i < tr.getCells().getLength(); i++) {
+ Element cell = createCell(tr.getCells().getItem(i));
+
+ editorOverlay.appendChild(cell);
+
+ Column<?, T> column = grid.getColumn(i);
+ if (column == grid.selectionColumn) {
+ continue;
+ }
+
+ Widget editor = getHandler().getWidget(column);
+ if (editor != null) {
+ columnToWidget.put(column, editor);
+ attachWidget(editor, cell);
+ }
+ }
+
+ saveButton = new Button();
+ saveButton.setText("Save");
+ saveButton.setStylePrimaryName("v-nativebutton");
+ saveButton.addStyleName(styleName + "-save");
+ saveButton.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ save();
+ }
+ });
+ setBounds(saveButton.getElement(), 0, tr.getOffsetHeight() + 5, 50,
+ 25);
+ attachWidget(saveButton, editorOverlay);
+
+ cancelButton = new Button();
+ cancelButton.setText("Cancel");
+ cancelButton.setStylePrimaryName("v-nativebutton");
+ cancelButton.addStyleName(styleName + "-cancel");
+ cancelButton.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ cancel();
+ }
+ });
+ setBounds(cancelButton.getElement(), 55, tr.getOffsetHeight() + 5,
+ 50, 25);
+ attachWidget(cancelButton, editorOverlay);
+ }
+
+ protected void hideOverlay() {
+ for (Widget w : columnToWidget.values()) {
+ setParent(w, null);
+ }
+ columnToWidget.clear();
+
+ editorOverlay.removeAllChildren();
+ editorOverlay.removeFromParent();
+
+ scrollHandler.removeHandler();
+ }
+
+ protected void setStylePrimaryName(String primaryName) {
+ if (styleName != null) {
+ editorOverlay.removeClassName(styleName);
+ }
+ styleName = primaryName + "-editor";
+ editorOverlay.addClassName(styleName);
+ }
+
+ /**
+ * Creates an editor cell corresponding to the given table cell. The
+ * returned element is empty and has the same dimensions and position as
+ * the table cell.
+ *
+ * @param td
+ * the table cell used as a reference
+ * @return an editor cell corresponding to the given cell
+ */
+ protected Element createCell(TableCellElement td) {
+ DivElement cell = DivElement.as(DOM.createDiv());
+ double width = WidgetUtil
+ .getRequiredWidthBoundingClientRectDouble(td);
+ double height = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(td);
+ setBounds(cell, td.getOffsetLeft(), td.getOffsetTop(), width,
+ height);
+ return cell;
+ }
+
+ private void attachWidget(Widget w, Element parent) {
+ parent.appendChild(w.getElement());
+ setParent(w, grid);
+ }
+
+ private static void setBounds(Element e, double left, double top,
+ double width, double height) {
+ Style style = e.getStyle();
+ style.setLeft(left, Unit.PX);
+ style.setTop(top, Unit.PX);
+ style.setWidth(width, Unit.PX);
+ style.setHeight(height, Unit.PX);
+ }
+
+ private void updateHorizontalScrollPosition() {
+ editorOverlay.getStyle().setLeft(-grid.getScrollLeft(), Unit.PX);
+ }
+
+ protected void setGridEnabled(boolean enabled) {
+ // TODO: This should be informed to handler as well so possible
+ // fields can be disabled.
+ setButtonsEnabled(enabled);
+ }
+
+ private void setButtonsEnabled(boolean enabled) {
+ saveButton.setEnabled(enabled);
+ cancelButton.setEnabled(enabled);
+ }
+ }
+
+ public static abstract class AbstractGridKeyEvent<HANDLER extends AbstractGridKeyEventHandler>
+ extends KeyEvent<HANDLER> {
+
+ private Grid<?> grid;
+ private final Type<HANDLER> associatedType = new Type<HANDLER>(
+ getBrowserEventType(), this);
+ private final CellReference<?> targetCell;
+
+ public AbstractGridKeyEvent(Grid<?> grid, CellReference<?> targetCell) {
+ this.grid = grid;
+ this.targetCell = targetCell;
+ }
+
+ protected abstract String getBrowserEventType();
+
+ /**
+ * Gets the Grid instance for this event.
+ *
+ * @return grid
+ */
+ public Grid<?> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the focused cell for this event.
+ *
+ * @return focused cell
+ */
+ public CellReference<?> getFocusedCell() {
+ return targetCell;
+ }
+
+ @Override
+ protected void dispatch(HANDLER handler) {
+ EventTarget target = getNativeEvent().getEventTarget();
+ if (Element.is(target)
+ && !grid.isElementInChildWidget(Element.as(target))) {
+
+ Section section = Section.FOOTER;
+ final RowContainer container = grid.cellFocusHandler.containerWithFocus;
+ if (container == grid.escalator.getHeader()) {
+ section = Section.HEADER;
+ } else if (container == grid.escalator.getBody()) {
+ section = Section.BODY;
+ }
+
+ doDispatch(handler, section);
+ }
+ }
+
+ protected abstract void doDispatch(HANDLER handler, Section section);
+
+ @Override
+ public Type<HANDLER> getAssociatedType() {
+ return associatedType;
+ }
+ }
+
+ public static abstract class AbstractGridMouseEvent<HANDLER extends AbstractGridMouseEventHandler>
+ extends MouseEvent<HANDLER> {
+
+ private Grid<?> grid;
+ private final CellReference<?> targetCell;
+ private final Type<HANDLER> associatedType = new Type<HANDLER>(
+ getBrowserEventType(), this);
+
+ public AbstractGridMouseEvent(Grid<?> grid, CellReference<?> targetCell) {
+ this.grid = grid;
+ this.targetCell = targetCell;
+ }
+
+ protected abstract String getBrowserEventType();
+
+ /**
+ * Gets the Grid instance for this event.
+ *
+ * @return grid
+ */
+ public Grid<?> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the reference of target cell for this event.
+ *
+ * @return target cell
+ */
+ public CellReference<?> getTargetCell() {
+ return targetCell;
+ }
+
+ @Override
+ protected void dispatch(HANDLER handler) {
+ EventTarget target = getNativeEvent().getEventTarget();
+ if (!Element.is(target)) {
+ // Target is not an element
+ return;
+ }
+
+ Element targetElement = Element.as(target);
+ if (grid.isElementInChildWidget(targetElement)) {
+ // Target is some widget inside of Grid
+ return;
+ }
+
+ final RowContainer container = grid.escalator
+ .findRowContainer(targetElement);
+ if (container == null) {
+ // No container for given element
+ return;
+ }
+
+ Section section = Section.FOOTER;
+ if (container == grid.escalator.getHeader()) {
+ section = Section.HEADER;
+ } else if (container == grid.escalator.getBody()) {
+ section = Section.BODY;
+ }
+
+ doDispatch(handler, section);
+ }
+
+ protected abstract void doDispatch(HANDLER handler, Section section);
+
+ @Override
+ public Type<HANDLER> getAssociatedType() {
+ return associatedType;
+ }
+ }
+
+ private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle";
+
+ private EventCellReference<T> eventCell = new EventCellReference<T>(this);
+ private GridKeyDownEvent keyDown = new GridKeyDownEvent(this, eventCell);
+ private GridKeyUpEvent keyUp = new GridKeyUpEvent(this, eventCell);
+ private GridKeyPressEvent keyPress = new GridKeyPressEvent(this, eventCell);
+ private GridClickEvent clickEvent = new GridClickEvent(this, eventCell);
+ private GridDoubleClickEvent doubleClickEvent = new GridDoubleClickEvent(
+ this, eventCell);
+
+ private class CellFocusHandler {
+
+ private RowContainer containerWithFocus = escalator.getBody();
+ private int rowWithFocus = 0;
+ private Range cellFocusRange = Range.withLength(0, 1);
+ private int lastFocusedBodyRow = 0;
+ private int lastFocusedHeaderRow = 0;
+ private int lastFocusedFooterRow = 0;
+ private TableCellElement cellWithFocusStyle = null;
+ private TableRowElement rowWithFocusStyle = null;
+
+ public CellFocusHandler() {
+ sinkEvents(getNavigationEvents());
+ }
+
+ private Cell getFocusedCell() {
+ return new Cell(rowWithFocus, cellFocusRange.getStart(),
+ cellWithFocusStyle);
+ }
+
+ /**
+ * Sets style names for given cell when needed.
+ */
+ public void updateFocusedCellStyle(FlyweightCell cell,
+ RowContainer cellContainer) {
+ int cellRow = cell.getRow();
+ int cellColumn = cell.getColumn();
+ int colSpan = cell.getColSpan();
+ boolean columnHasFocus = Range.withLength(cellColumn, colSpan)
+ .intersects(cellFocusRange);
+
+ if (cellContainer == containerWithFocus) {
+ // Cell is in the current container
+ if (cellRow == rowWithFocus && columnHasFocus) {
+ if (cellWithFocusStyle != cell.getElement()) {
+ // Cell is correct but it does not have focused style
+ if (cellWithFocusStyle != null) {
+ // Remove old focus style
+ setStyleName(cellWithFocusStyle,
+ cellFocusStyleName, false);
+ }
+ cellWithFocusStyle = cell.getElement();
+
+ // Add focus style to correct cell.
+ setStyleName(cellWithFocusStyle, cellFocusStyleName,
+ true);
+ }
+ } else if (cellWithFocusStyle == cell.getElement()) {
+ // Due to escalator reusing cells, a new cell has the same
+ // element but is not the focused cell.
+ setStyleName(cellWithFocusStyle, cellFocusStyleName, false);
+ cellWithFocusStyle = null;
+ }
+ }
+ }
+
+ /**
+ * Sets focus style for the given row if needed.
+ *
+ * @param row
+ * a row object
+ */
+ public void updateFocusedRowStyle(Row row) {
+ if (rowWithFocus == row.getRow()
+ && containerWithFocus == escalator.getBody()) {
+ if (row.getElement() != rowWithFocusStyle) {
+ // Row should have focus style but does not have it.
+ if (rowWithFocusStyle != null) {
+ setStyleName(rowWithFocusStyle, rowFocusStyleName,
+ false);
+ }
+ rowWithFocusStyle = row.getElement();
+ setStyleName(rowWithFocusStyle, rowFocusStyleName, true);
+ }
+ } else if (rowWithFocusStyle == row.getElement()
+ || (containerWithFocus != escalator.getBody() && rowWithFocusStyle != null)) {
+ // Remove focus style.
+ setStyleName(rowWithFocusStyle, rowFocusStyleName, false);
+ rowWithFocusStyle = null;
+ }
+ }
+
+ /**
+ * Sets the currently focused.
+ *
+ * @param row
+ * the index of the row having focus
+ * @param column
+ * the index of the column having focus
+ * @param container
+ * the row container having focus
+ */
+ private void setCellFocus(int row, int column, RowContainer container) {
+ if (row == rowWithFocus && cellFocusRange.contains(column)
+ && container == this.containerWithFocus) {
+ refreshRow(rowWithFocus);
+ return;
+ }
+
+ int oldRow = rowWithFocus;
+ rowWithFocus = row;
+ Range oldRange = cellFocusRange;
+
+ if (container == escalator.getBody()) {
+ scrollToRow(rowWithFocus);
+ cellFocusRange = Range.withLength(column, 1);
+ } else {
+ int i = 0;
+ Element cell = container.getRowElement(rowWithFocus)
+ .getFirstChildElement();
+ do {
+ int colSpan = cell
+ .getPropertyInt(FlyweightCell.COLSPAN_ATTR);
+ Range cellRange = Range.withLength(i, colSpan);
+ if (cellRange.contains(column)) {
+ cellFocusRange = cellRange;
+ break;
+ }
+ cell = cell.getNextSiblingElement();
+ ++i;
+ } while (cell != null);
+ }
+
+ if (column >= escalator.getColumnConfiguration()
+ .getFrozenColumnCount()) {
+ escalator.scrollToColumn(column, ScrollDestination.ANY, 10);
+ }
+
+ if (this.containerWithFocus == container) {
+ if (oldRange.equals(cellFocusRange) && oldRow != rowWithFocus) {
+ refreshRow(oldRow);
+ } else {
+ refreshHeader();
+ refreshFooter();
+ }
+ } else {
+ RowContainer oldContainer = this.containerWithFocus;
+ this.containerWithFocus = container;
+
+ if (oldContainer == escalator.getBody()) {
+ lastFocusedBodyRow = oldRow;
+ } else if (oldContainer == escalator.getHeader()) {
+ lastFocusedHeaderRow = oldRow;
+ } else {
+ lastFocusedFooterRow = oldRow;
+ }
+
+ if (!oldRange.equals(cellFocusRange)) {
+ refreshHeader();
+ refreshFooter();
+ if (oldContainer == escalator.getBody()) {
+ oldContainer.refreshRows(oldRow, 1);
+ }
+ } else {
+ oldContainer.refreshRows(oldRow, 1);
+ }
+ }
+ refreshRow(rowWithFocus);
+ }
+
+ /**
+ * Sets focus on a cell.
+ *
+ * <p>
+ * <em>Note</em>: cell focus is not the same as JavaScript's
+ * {@code document.activeElement}.
+ *
+ * @param cell
+ * a cell object
+ */
+ public void setCellFocus(CellReference<T> cell) {
+ setCellFocus(cell.getRowIndex(), cell.getColumnIndex(),
+ escalator.findRowContainer(cell.getElement()));
+ }
+
+ /**
+ * Gets list of events that can be used for cell focusing.
+ *
+ * @return list of navigation related event types
+ */
+ public Collection<String> getNavigationEvents() {
+ return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK);
+ }
+
+ /**
+ * Handle events that can move the cell focus.
+ */
+ public void handleNavigationEvent(Event event, CellReference<T> cell) {
+ if (event.getType().equals(BrowserEvents.CLICK)) {
+ setCellFocus(cell);
+ // Grid should have focus when clicked.
+ getElement().focus();
+ } else if (event.getType().equals(BrowserEvents.KEYDOWN)) {
+ int newRow = rowWithFocus;
+ RowContainer newContainer = containerWithFocus;
+ int newColumn = cellFocusRange.getStart();
+
+ switch (event.getKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ ++newRow;
+ break;
+ case KeyCodes.KEY_UP:
+ --newRow;
+ break;
+ case KeyCodes.KEY_RIGHT:
+ if (cellFocusRange.getEnd() >= getColumns().size()) {
+ return;
+ }
+ newColumn = cellFocusRange.getEnd();
+ break;
+ case KeyCodes.KEY_LEFT:
+ if (newColumn == 0) {
+ return;
+ }
+ --newColumn;
+ break;
+ case KeyCodes.KEY_TAB:
+ if (event.getShiftKey()) {
+ newContainer = getPreviousContainer(containerWithFocus);
+ } else {
+ newContainer = getNextContainer(containerWithFocus);
+ }
+
+ if (newContainer == containerWithFocus) {
+ return;
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (newContainer != containerWithFocus) {
+ if (newContainer == escalator.getBody()) {
+ newRow = lastFocusedBodyRow;
+ } else if (newContainer == escalator.getHeader()) {
+ newRow = lastFocusedHeaderRow;
+ } else {
+ newRow = lastFocusedFooterRow;
+ }
+ } else if (newRow < 0) {
+ newContainer = getPreviousContainer(newContainer);
+
+ if (newContainer == containerWithFocus) {
+ newRow = 0;
+ } else if (newContainer == escalator.getBody()) {
+ newRow = getLastVisibleRowIndex();
+ } else {
+ newRow = newContainer.getRowCount() - 1;
+ }
+ } else if (newRow >= containerWithFocus.getRowCount()) {
+ newContainer = getNextContainer(newContainer);
+
+ if (newContainer == containerWithFocus) {
+ newRow = containerWithFocus.getRowCount() - 1;
+ } else if (newContainer == escalator.getBody()) {
+ newRow = getFirstVisibleRowIndex();
+ } else {
+ newRow = 0;
+ }
+ }
+
+ if (newContainer.getRowCount() == 0) {
+ /*
+ * There are no rows in the container. Can't change the
+ * focused cell.
+ */
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ setCellFocus(newRow, newColumn, newContainer);
+ }
+
+ }
+
+ private RowContainer getPreviousContainer(RowContainer current) {
+ if (current == escalator.getFooter()) {
+ current = escalator.getBody();
+ } else if (current == escalator.getBody()) {
+ current = escalator.getHeader();
+ } else {
+ return current;
+ }
+
+ if (current.getRowCount() == 0) {
+ return getPreviousContainer(current);
+ }
+ return current;
+ }
+
+ private RowContainer getNextContainer(RowContainer current) {
+ if (current == escalator.getHeader()) {
+ current = escalator.getBody();
+ } else if (current == escalator.getBody()) {
+ current = escalator.getFooter();
+ } else {
+ return current;
+ }
+
+ if (current.getRowCount() == 0) {
+ return getNextContainer(current);
+ }
+ return current;
+ }
+
+ private void refreshRow(int row) {
+ containerWithFocus.refreshRows(row, 1);
+ }
+
+ /**
+ * Offsets the focused cell's range.
+ *
+ * @param offset
+ * offset for fixing focused cell's range
+ */
+ public void offsetRangeBy(int offset) {
+ cellFocusRange = cellFocusRange.offsetBy(offset);
+ }
+
+ /**
+ * Informs {@link CellFocusHandler} that certain range of rows has been
+ * added to the Grid body. {@link CellFocusHandler} will fix indices
+ * accordingly.
+ *
+ * @param added
+ * a range of added rows
+ */
+ public void rowsAddedToBody(Range added) {
+ boolean bodyHasFocus = (containerWithFocus == escalator.getBody());
+ boolean insertionIsAboveFocusedCell = (added.getStart() <= rowWithFocus);
+ if (bodyHasFocus && insertionIsAboveFocusedCell) {
+ rowWithFocus += added.length();
+ refreshRow(rowWithFocus);
+ }
+ }
+
+ /**
+ * Informs {@link CellFocusHandler} that certain range of rows has been
+ * removed from the Grid body. {@link CellFocusHandler} will fix indices
+ * accordingly.
+ *
+ * @param removed
+ * a range of removed rows
+ */
+ public void rowsRemovedFromBody(Range removed) {
+ if (containerWithFocus != escalator.getBody()) {
+ return;
+ } else if (!removed.contains(rowWithFocus)) {
+ if (removed.getStart() > rowWithFocus) {
+ return;
+ }
+ rowWithFocus = rowWithFocus - removed.length();
+ } else {
+ if (containerWithFocus.getRowCount() > removed.getEnd()) {
+ rowWithFocus = removed.getStart();
+ } else if (removed.getStart() > 0) {
+ rowWithFocus = removed.getStart() - 1;
+ } else {
+ if (escalator.getHeader().getRowCount() > 0) {
+ rowWithFocus = lastFocusedHeaderRow;
+ containerWithFocus = escalator.getHeader();
+ } else if (escalator.getFooter().getRowCount() > 0) {
+ rowWithFocus = lastFocusedFooterRow;
+ containerWithFocus = escalator.getFooter();
+ }
+ }
+ }
+ refreshRow(rowWithFocus);
+ }
+ }
+
+ public final class SelectionColumn extends Column<Boolean, T> {
+ private boolean initDone = false;
+
+ SelectionColumn(final Renderer<Boolean> selectColumnRenderer) {
+ super(selectColumnRenderer);
+ }
+
+ void initDone() {
+ if (getSelectionModel() instanceof SelectionModel.Multi
+ && header.getDefaultRow() != null) {
+ /*
+ * TODO: Currently the select all check box is shown when multi
+ * selection is in use. This might result in malfunctions if no
+ * SelectAllHandlers are present.
+ *
+ * Later on this could be fixed so that it check such handlers
+ * exist.
+ */
+ final SelectionModel.Multi<T> model = (Multi<T>) getSelectionModel();
+ final CheckBox checkBox = new CheckBox();
+ checkBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ if (event.getValue()) {
+ fireEvent(new SelectAllEvent<T>(model));
+ } else {
+ model.deselectAll();
+ }
+ }
+ });
+ header.getDefaultRow().getCell(this).setWidget(checkBox);
+ }
+
+ setWidth(-1);
+
+ initDone = true;
+ }
+
+ @Override
+ public Column<Boolean, T> setWidth(double pixels) {
+ if (pixels != getWidth() && initDone) {
+ throw new UnsupportedOperationException("The selection "
+ + "column cannot be modified after init");
+ } else {
+ super.setWidth(pixels);
+ }
+
+ return this;
+ }
+
+ @Override
+ public Boolean getValue(T row) {
+ return Boolean.valueOf(isSelected(row));
+ }
+
+ @Override
+ public Column<Boolean, T> setExpandRatio(int ratio) {
+ throw new UnsupportedOperationException(
+ "can't change the expand ratio of the selection column");
+ }
+
+ @Override
+ public int getExpandRatio() {
+ return 0;
+ }
+
+ @Override
+ public Column<Boolean, T> setMaximumWidth(double pixels) {
+ throw new UnsupportedOperationException(
+ "can't change the maximum width of the selection column");
+ }
+
+ @Override
+ public double getMaximumWidth() {
+ return -1;
+ }
+
+ @Override
+ public Column<Boolean, T> setMinimumWidth(double pixels) {
+ throw new UnsupportedOperationException(
+ "can't change the minimum width of the selection column");
+ }
+
+ @Override
+ public double getMinimumWidth() {
+ return -1;
+ }
+ }
+
+ /**
+ * Helper class for performing sorting through the user interface. Controls
+ * the sort() method, reporting USER as the event originator. This is a
+ * completely internal class, and is, as such, safe to re-name should a more
+ * descriptive name come to mind.
+ */
+ private final class UserSorter {
+
+ private final Timer timer;
+ private boolean scheduledMultisort;
+ private Column<?, T> column;
+
+ private UserSorter() {
+ timer = new Timer() {
+
+ @Override
+ public void run() {
+ UserSorter.this.sort(column, scheduledMultisort);
+ }
+ };
+ }
+
+ /**
+ * Toggle sorting for a cell. If the multisort parameter is set to true,
+ * the cell's sort order is modified as a natural part of a multi-sort
+ * chain. If false, the sorting order is set to ASCENDING for that
+ * cell's column. If that column was already the only sorted column in
+ * the Grid, the sort direction is flipped.
+ *
+ * @param cell
+ * a valid cell reference
+ * @param multisort
+ * whether the sort command should act as a multi-sort stack
+ * or not
+ */
+ public void sort(Column<?, ?> column, boolean multisort) {
+
+ if (!columns.contains(column)) {
+ throw new IllegalArgumentException(
+ "Given column is not a column in this grid. "
+ + column.toString());
+ }
+
+ if (!column.isSortable()) {
+ return;
+ }
+
+ final SortOrder so = getSortOrder(column);
+
+ if (multisort) {
+
+ // If the sort order exists, replace existing value with its
+ // opposite
+ if (so != null) {
+ final int idx = sortOrder.indexOf(so);
+ sortOrder.set(idx, so.getOpposite());
+ } else {
+ // If it doesn't, just add a new sort order to the end of
+ // the list
+ sortOrder.add(new SortOrder(column));
+ }
+
+ } else {
+
+ // Since we're doing single column sorting, first clear the
+ // list. Then, if the sort order existed, add its opposite,
+ // otherwise just add a new sort value
+
+ int items = sortOrder.size();
+ sortOrder.clear();
+ if (so != null && items == 1) {
+ sortOrder.add(so.getOpposite());
+ } else {
+ sortOrder.add(new SortOrder(column));
+ }
+ }
+
+ // sortOrder has been changed; tell the Grid to re-sort itself by
+ // user request.
+ Grid.this.sort(true);
+ }
+
+ /**
+ * Perform a sort after a delay.
+ *
+ * @param delay
+ * delay, in milliseconds
+ */
+ public void sortAfterDelay(int delay, boolean multisort) {
+ column = eventCell.getColumn();
+ scheduledMultisort = multisort;
+ timer.schedule(delay);
+ }
+
+ /**
+ * Check if a delayed sort command has been issued but not yet carried
+ * out.
+ *
+ * @return a boolean value
+ */
+ public boolean isDelayedSortScheduled() {
+ return timer.isRunning();
+ }
+
+ /**
+ * Cancel a scheduled sort.
+ */
+ public void cancelDelayedSort() {
+ timer.cancel();
+ }
+
+ }
+
+ /** @see Grid#autoColumnWidthsRecalculator */
+ private class AutoColumnWidthsRecalculator {
+
+ private final ScheduledCommand calculateCommand = new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (!isScheduled) {
+ // something cancelled running this.
+ return;
+ }
+
+ if (header.markAsDirty || footer.markAsDirty) {
+ if (rescheduleCount < 10) {
+ /*
+ * Headers and footers are rendered as finally, this way
+ * we re-schedule this loop as finally, at the end of
+ * the queue, so that the headers have a chance to
+ * render themselves.
+ */
+ Scheduler.get().scheduleFinally(this);
+ rescheduleCount++;
+ } else {
+ /*
+ * We've tried too many times reschedule finally. Seems
+ * like something is being deferred. Let the queue
+ * execute and retry again.
+ */
+ rescheduleCount = 0;
+ Scheduler.get().scheduleDeferred(this);
+ }
+ } else if (dataIsBeingFetched) {
+ Scheduler.get().scheduleDeferred(this);
+ } else {
+ calculate();
+ }
+ }
+ };
+
+ private int rescheduleCount = 0;
+ private boolean isScheduled;
+
+ /**
+ * Calculates and applies column widths, taking into account fixed
+ * widths and column expand rules
+ *
+ * @param immediately
+ * <code>true</code> if the widths should be executed
+ * immediately (ignoring lazy loading completely), or
+ * <code>false</code> if the command should be run after a
+ * while (duplicate non-immediately invocations are ignored).
+ * @see Column#setWidth(double)
+ * @see Column#setExpandRatio(int)
+ * @see Column#setMinimumWidth(double)
+ * @see Column#setMaximumWidth(double)
+ */
+ public void schedule() {
+ if (!isScheduled) {
+ isScheduled = true;
+ Scheduler.get().scheduleFinally(calculateCommand);
+ }
+ }
+
+ private void calculate() {
+ isScheduled = false;
+ rescheduleCount = 0;
+
+ assert !dataIsBeingFetched : "Trying to calculate column widths even though data is still being fetched.";
+ /*
+ * At this point we assume that no data is being fetched anymore.
+ * Everything's rendered in the DOM. Now we just make sure
+ * everything fits as it should.
+ */
+
+ /*
+ * Quick optimization: if the sum of fixed widths and minimum widths
+ * is greater than the grid can display, we already know that things
+ * will be squeezed and no expansion will happen.
+ */
+ if (gridWasTooNarrowAndEverythingWasFixedAlready()) {
+ return;
+ }
+
+ boolean someColumnExpands = false;
+ int totalRatios = 0;
+ double reservedPixels = 0;
+ final Set<Column<?, ?>> columnsToExpand = new HashSet<Column<?, ?>>();
+
+ /*
+ * Set all fixed widths and also calculate the size-to-fit widths
+ * for the autocalculated columns.
+ *
+ * This way we know with how many pixels we have left to expand the
+ * rest.
+ */
+ for (Column<?, ?> column : getColumns()) {
+ final double widthAsIs = column.getWidth();
+ final boolean isFixedWidth = widthAsIs >= 0;
+ final double widthFixed = Math.max(widthAsIs,
+ column.getMinimumWidth());
+ final int expandRatio = column.getExpandRatio();
+
+ if (isFixedWidth) {
+ column.doSetWidth(widthFixed);
+ } else {
+ column.doSetWidth(-1);
+ final double newWidth = column.getWidthActual();
+ final double maxWidth = getMaxWidth(column);
+ boolean shouldExpand = newWidth < maxWidth
+ && expandRatio > 0;
+ if (shouldExpand) {
+ totalRatios += expandRatio;
+ columnsToExpand.add(column);
+ someColumnExpands = true;
+ }
+ }
+ reservedPixels += column.getWidthActual();
+ }
+
+ /*
+ * If no column has a positive expand ratio, all columns with a
+ * negative expand ratio has an expand ratio. Columns with 0 expand
+ * ratio are excluded.
+ *
+ * This means that if we only define one column to have 0 expand, it
+ * will be the only one not to expand, while all the others expand.
+ */
+ if (!someColumnExpands) {
+ assert totalRatios == 0 : "totalRatios should've been 0";
+ assert columnsToExpand.isEmpty() : "columnsToExpand should've been empty";
+ for (Column<?, ?> column : getColumns()) {
+ final double width = column.getWidth();
+ final int expandRatio = column.getExpandRatio();
+ if (width < 0 && expandRatio < 0) {
+ totalRatios++;
+ columnsToExpand.add(column);
+ }
+ }
+ }
+
+ /*
+ * Now that we know how many pixels we need at the very least, we
+ * can distribute the remaining pixels to all columns according to
+ * their expand ratios.
+ */
+ double pixelsToDistribute = escalator.getInnerWidth()
+ - reservedPixels;
+ if (pixelsToDistribute <= 0 || totalRatios <= 0) {
+ return;
+ }
+
+ /*
+ * Check for columns that hit their max width. Adjust
+ * pixelsToDistribute and totalRatios accordingly. Recheck. Stop
+ * when no new columns hit their max width
+ */
+ boolean aColumnHasMaxedOut;
+ do {
+ aColumnHasMaxedOut = false;
+ final double widthPerRatio = pixelsToDistribute / totalRatios;
+ final Iterator<Column<?, ?>> i = columnsToExpand.iterator();
+ while (i.hasNext()) {
+ final Column<?, ?> column = i.next();
+ final int expandRatio = getExpandRatio(column,
+ someColumnExpands);
+ final double autoWidth = column.getWidthActual();
+ final double maxWidth = getMaxWidth(column);
+ final double widthCandidate = autoWidth + widthPerRatio
+ * expandRatio;
+
+ if (maxWidth <= widthCandidate) {
+ column.doSetWidth(maxWidth);
+ totalRatios -= expandRatio;
+ pixelsToDistribute -= maxWidth - autoWidth;
+ i.remove();
+ aColumnHasMaxedOut = true;
+ }
+ }
+ } while (aColumnHasMaxedOut);
+
+ if (totalRatios <= 0 && columnsToExpand.isEmpty()) {
+ return;
+ }
+ assert pixelsToDistribute > 0 : "We've run out of pixels to distribute ("
+ + pixelsToDistribute
+ + "px to "
+ + totalRatios
+ + " ratios between " + columnsToExpand.size() + " columns)";
+ assert totalRatios > 0 && !columnsToExpand.isEmpty() : "Bookkeeping out of sync. Ratios: "
+ + totalRatios + " Columns: " + columnsToExpand.size();
+
+ /*
+ * If we still have anything left, distribute the remaining pixels
+ * to the remaining columns.
+ */
+ final double widthPerRatio = pixelsToDistribute / totalRatios;
+ for (Column<?, ?> column : columnsToExpand) {
+ final int expandRatio = getExpandRatio(column,
+ someColumnExpands);
+ final double autoWidth = column.getWidthActual();
+ final double totalWidth = autoWidth + widthPerRatio
+ * expandRatio;
+ column.doSetWidth(totalWidth);
+
+ totalRatios -= expandRatio;
+ }
+ assert totalRatios == 0 : "Bookkeeping error: there were still some ratios left undistributed: "
+ + totalRatios;
+
+ /*
+ * Check the guarantees for minimum width and scoot back the columns
+ * that don't care.
+ */
+ boolean minWidthsCausedReflows;
+ do {
+ minWidthsCausedReflows = false;
+
+ /*
+ * First, let's check which columns were too cramped, and expand
+ * them. Also keep track on how many pixels we grew - we need to
+ * remove those pixels from other columns
+ */
+ double pixelsToRemoveFromOtherColumns = 0;
+ for (Column<?, T> column : getColumns()) {
+ /*
+ * We can't iterate over columnsToExpand, even though that
+ * would be convenient. This is because some column without
+ * an expand ratio might still have a min width - those
+ * wouldn't show up in that set.
+ */
+
+ double minWidth = getMinWidth(column);
+ double currentWidth = column.getWidthActual();
+ boolean hasAutoWidth = column.getWidth() < 0;
+ if (hasAutoWidth && currentWidth < minWidth) {
+ column.doSetWidth(minWidth);
+ pixelsToRemoveFromOtherColumns += (minWidth - currentWidth);
+ minWidthsCausedReflows = true;
+
+ /*
+ * Remove this column form the set if it exists. This
+ * way we make sure that it doesn't get shrunk in the
+ * next step.
+ */
+ columnsToExpand.remove(column);
+ }
+ }
+
+ /*
+ * Now we need to shrink the remaining columns according to
+ * their ratios. Recalculate the sum of remaining ratios.
+ */
+ totalRatios = 0;
+ for (Column<?, ?> column : columnsToExpand) {
+ totalRatios += getExpandRatio(column, someColumnExpands);
+ }
+ final double pixelsToRemovePerRatio = pixelsToRemoveFromOtherColumns
+ / totalRatios;
+ for (Column<?, ?> column : columnsToExpand) {
+ final double pixelsToRemove = pixelsToRemovePerRatio
+ * getExpandRatio(column, someColumnExpands);
+ column.doSetWidth(column.getWidthActual() - pixelsToRemove);
+ }
+
+ } while (minWidthsCausedReflows);
+ }
+
+ private boolean gridWasTooNarrowAndEverythingWasFixedAlready() {
+ double freeSpace = escalator.getInnerWidth();
+ for (Column<?, ?> column : getColumns()) {
+ if (column.getWidth() >= 0) {
+ freeSpace -= column.getWidth();
+ } else if (column.getMinimumWidth() >= 0) {
+ freeSpace -= column.getMinimumWidth();
+ }
+ }
+
+ if (freeSpace < 0) {
+ for (Column<?, ?> column : getColumns()) {
+ column.doSetWidth(column.getWidth());
+
+ boolean wasFixedWidth = column.getWidth() <= 0;
+ boolean newWidthIsSmallerThanMinWidth = column
+ .getWidthActual() < getMinWidth(column);
+ if (wasFixedWidth && newWidthIsSmallerThanMinWidth) {
+ column.doSetWidth(column.getMinimumWidth());
+ }
+ }
+ }
+
+ return freeSpace < 0;
+ }
+
+ private int getExpandRatio(Column<?, ?> column,
+ boolean someColumnExpands) {
+ int expandRatio = column.getExpandRatio();
+ if (expandRatio > 0) {
+ return expandRatio;
+ } else if (expandRatio < 0) {
+ assert !someColumnExpands : "No columns should've expanded";
+ return 1;
+ } else {
+ assert false : "this method should've not been called at all if expandRatio is 0";
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the maximum width of the column, or {@link Double#MAX_VALUE}
+ * if defined as negative.
+ */
+ private double getMaxWidth(Column<?, ?> column) {
+ double maxWidth = column.getMaximumWidth();
+ if (maxWidth >= 0) {
+ return maxWidth;
+ } else {
+ return Double.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Returns the minimum width of the column, or {@link Double#MIN_VALUE}
+ * if defined as negative.
+ */
+ private double getMinWidth(Column<?, ?> column) {
+ double minWidth = column.getMinimumWidth();
+ if (minWidth >= 0) {
+ return minWidth;
+ } else {
+ return Double.MIN_VALUE;
+ }
+ }
+
+ /**
+ * Check whether the auto width calculation is currently scheduled.
+ *
+ * @return <code>true</code> if auto width calculation is currently
+ * scheduled
+ */
+ public boolean isScheduled() {
+ return isScheduled;
+ }
+ }
+
+ /**
+ * Escalator used internally by grid to render the rows
+ */
+ private Escalator escalator = GWT.create(Escalator.class);
+
+ private final Header header = GWT.create(Header.class);
+
+ private final Footer footer = GWT.create(Footer.class);
+
+ /**
+ * List of columns in the grid. Order defines the visible order.
+ */
+ private List<Column<?, T>> columns = new ArrayList<Column<?, T>>();
+
+ /**
+ * The datasource currently in use. <em>Note:</em> it is <code>null</code>
+ * on initialization, but not after that.
+ */
+ private DataSource<T> dataSource;
+
+ /**
+ * Currently available row range in DataSource.
+ */
+ private Range currentDataAvailable = Range.withLength(0, 0);
+
+ /**
+ * The number of frozen columns, 0 freezes the selection column if
+ * displayed, -1 also prevents selection col from freezing.
+ */
+ private int frozenColumnCount = 0;
+
+ /**
+ * Current sort order. The (private) sort() method reads this list to
+ * determine the order in which to present rows.
+ */
+ private List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ private Renderer<Boolean> selectColumnRenderer = null;
+
+ private SelectionColumn selectionColumn;
+
+ private String rowStripeStyleName;
+ private String rowHasDataStyleName;
+ private String rowSelectedStyleName;
+ private String cellFocusStyleName;
+ private String rowFocusStyleName;
+
+ /**
+ * Current selection model.
+ */
+ private SelectionModel<T> selectionModel;
+
+ protected final CellFocusHandler cellFocusHandler;
+
+ private final UserSorter sorter = new UserSorter();
+
+ private final Editor<T> editor = GWT.create(Editor.class);
+
+ private boolean dataIsBeingFetched = false;
+
+ /**
+ * The cell a click event originated from
+ * <p>
+ * This is a workaround to make Chrome work like Firefox. In Chrome,
+ * normally if you start a drag on one cell and release on:
+ * <ul>
+ * <li>that same cell, the click event is that {@code <td>}.
+ * <li>a cell on that same row, the click event is the parent {@code <tr>}.
+ * <li>a cell on another row, the click event is the table section ancestor
+ * ({@code <thead>}, {@code <tbody>} or {@code <tfoot>}).
+ * </ul>
+ *
+ * @see #onBrowserEvent(Event)
+ */
+ private Cell cellOnPrevMouseDown;
+
+ /**
+ * A scheduled command to re-evaluate the widths of <em>all columns</em>
+ * that have calculated widths. Most probably called because
+ * minwidth/maxwidth/expandratio has changed.
+ */
+ private final AutoColumnWidthsRecalculator autoColumnWidthsRecalculator = new AutoColumnWidthsRecalculator();
+
+ private boolean enabled = true;
+
+ /**
+ * Enumeration for easy setting of selection mode.
+ */
+ public enum SelectionMode {
+
+ /**
+ * Shortcut for {@link SelectionModelSingle}.
+ */
+ SINGLE {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return new SelectionModelSingle<T>();
+ }
+ },
+
+ /**
+ * Shortcut for {@link SelectionModelMulti}.
+ */
+ MULTI {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return new SelectionModelMulti<T>();
+ }
+ },
+
+ /**
+ * Shortcut for {@link SelectionModelNone}.
+ */
+ NONE {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return new SelectionModelNone<T>();
+ }
+ };
+
+ protected abstract <T> SelectionModel<T> createModel();
+ }
+
+ /**
+ * Base class for grid columns internally used by the Grid. The user should
+ * use {@link Column} when creating new columns.
+ *
+ * @param <C>
+ * the column type
+ *
+ * @param <T>
+ * the row type
+ */
+ public static abstract class Column<C, T> {
+
+ /**
+ * The default renderer for grid columns.
+ * <p>
+ * The first time this renderer is called, a warning is displayed,
+ * informing the developer to use a manually defined renderer for their
+ * column.
+ */
+ private final class DefaultObjectRenderer extends ObjectRenderer {
+ boolean warned = false;
+ private final String DEFAULT_RENDERER_WARNING = "This column uses "
+ + "a dummy default ObjectRenderer. A more suitable "
+ + "renderer should be set using the setRenderer() "
+ + "method.";
+
+ @Override
+ public void render(RendererCellReference cell, Object data) {
+ if (!warned && !(data instanceof String)) {
+ getLogger().warning(
+ Column.this.toString() + ": "
+ + DEFAULT_RENDERER_WARNING);
+ warned = true;
+ }
+
+ super.render(cell, data);
+ }
+ }
+
+ /**
+ * the column is associated with
+ */
+ private Grid<T> grid;
+
+ /**
+ * Width of column in pixels as {@link #setWidth(double)} has been
+ * called
+ */
+ private double widthUser = GridConstants.DEFAULT_COLUMN_WIDTH_PX;
+
+ /**
+ * Renderer for rendering a value into the cell
+ */
+ private Renderer<? super C> bodyRenderer;
+
+ private boolean sortable = false;
+
+ private String headerCaption = "";
+
+ private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH;
+ private double maximumWidthPx = GridConstants.DEFAULT_MAX_WIDTH;
+ private int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO;
+
+ /**
+ * Constructs a new column with a simple TextRenderer.
+ */
+ public Column() {
+ setRenderer(new DefaultObjectRenderer());
+ }
+
+ /**
+ * Constructs a new column with a simple TextRenderer.
+ *
+ * @param caption
+ * The header caption for this column
+ *
+ * @throws IllegalArgumentException
+ * if given header caption is null
+ */
+ public Column(String caption) throws IllegalArgumentException {
+ this();
+ setHeaderCaption(caption);
+ }
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ *
+ * @throws IllegalArgumentException
+ * if given Renderer is null
+ */
+ public Column(Renderer<? super C> renderer)
+ throws IllegalArgumentException {
+ setRenderer(renderer);
+ }
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ * @param caption
+ * The header caption for this column
+ *
+ * @throws IllegalArgumentException
+ * if given Renderer or header caption is null
+ */
+ public Column(String caption, Renderer<? super C> renderer)
+ throws IllegalArgumentException {
+ this(renderer);
+ setHeaderCaption(caption);
+ }
+
+ /**
+ * Internally used by the grid to set itself
+ *
+ * @param grid
+ */
+ private void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException("Column already is attached "
+ + "to a grid. Remove the column first from the grid "
+ + "and then add it. (in: " + toString() + ")");
+ }
+
+ if (this.grid != null) {
+ this.grid.autoColumnWidthsRecalculator.schedule();
+ }
+ this.grid = grid;
+ if (this.grid != null) {
+ this.grid.autoColumnWidthsRecalculator.schedule();
+ updateHeader();
+ }
+ }
+
+ /**
+ * Sets a header caption for this column.
+ *
+ * @param caption
+ * The header caption for this column
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if given caption text is null
+ */
+ public Column<C, T> setHeaderCaption(String caption) {
+ if (caption == null) {
+ throw new IllegalArgumentException("Caption cannot be null.");
+ }
+
+ if (!this.headerCaption.equals(caption)) {
+ this.headerCaption = caption;
+ if (grid != null) {
+ updateHeader();
+ }
+ }
+
+ return this;
+ }
+
+ private void updateHeader() {
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ row.getCell(this).setText(headerCaption);
+ }
+ }
+
+ /**
+ * Returns the data that should be rendered into the cell. By default
+ * returning Strings and Widgets are supported. If the return type is a
+ * String then it will be treated as preformatted text.
+ * <p>
+ * To support other types you will need to pass a custom renderer to the
+ * column via the column constructor.
+ *
+ * @param row
+ * The row object that provides the cell content.
+ *
+ * @return The cell content
+ */
+ public abstract C getValue(T row);
+
+ /**
+ * The renderer to render the cell width. By default renders the data as
+ * a String or adds the widget into the cell if the column type is of
+ * widget type.
+ *
+ * @return The renderer to render the cell content with
+ */
+ public Renderer<? super C> getRenderer() {
+ return bodyRenderer;
+ }
+
+ /**
+ * Sets a custom {@link Renderer} for this column.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if given Renderer is null
+ */
+ public Column<C, T> setRenderer(Renderer<? super C> renderer)
+ throws IllegalArgumentException {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ bodyRenderer = renderer;
+
+ if (grid != null) {
+ grid.refreshBody();
+ }
+
+ return this;
+ }
+
+ /**
+ * Sets the pixel width of the column. Use a negative value for the grid
+ * to autosize column based on content and available space.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param pixels
+ * the width in pixels or negative for auto sizing
+ */
+ public Column<C, T> setWidth(double pixels) {
+ if (widthUser != pixels) {
+ widthUser = pixels;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ void doSetWidth(double pixels) {
+ if (grid != null) {
+ int index = grid.columns.indexOf(this);
+ ColumnConfiguration conf = grid.escalator
+ .getColumnConfiguration();
+ conf.setColumnWidth(index, pixels);
+ }
+ }
+
+ /**
+ * Returns the pixel width of the column as given by the user.
+ * <p>
+ * <em>Note:</em> If a negative value was given to
+ * {@link #setWidth(double)}, that same negative value is returned here.
+ *
+ * @return pixel width of the column, or a negative number if the column
+ * width has been automatically calculated.
+ * @see #setWidth(double)
+ * @see #getWidthActual()
+ */
+ public double getWidth() {
+ return widthUser;
+ }
+
+ /**
+ * Returns the effective pixel width of the column.
+ * <p>
+ * This differs from {@link #getWidth()} only when the column has been
+ * automatically resized.
+ *
+ * @return pixel width of the column.
+ */
+ public double getWidthActual() {
+ return grid.escalator.getColumnConfiguration()
+ .getColumnWidthActual(grid.columns.indexOf(this));
+ }
+
+ void reapplyWidth() {
+ setWidth(getWidth());
+ }
+
+ /**
+ * Enables sort indicators for the grid.
+ * <p>
+ * <b>Note:</b>The API can still sort the column even if this is set to
+ * <code>false</code>.
+ *
+ * @param sortable
+ * <code>true</code> when column sort indicators are visible.
+ * @return the column itself
+ */
+ public Column<C, T> setSortable(boolean sortable) {
+ if (this.sortable != sortable) {
+ this.sortable = sortable;
+ if (grid != null) {
+ grid.refreshHeader();
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Are sort indicators shown for the column.
+ *
+ * @return <code>true</code> if the column is sortable
+ */
+ public boolean isSortable() {
+ return sortable;
+ }
+
+ @Override
+ public String toString() {
+ String details = "";
+
+ if (headerCaption != null && !headerCaption.isEmpty()) {
+ details += "header:\"" + headerCaption + "\" ";
+ } else {
+ details += "header:empty ";
+ }
+
+ if (grid != null) {
+ int index = grid.getColumns().indexOf(this);
+ if (index != -1) {
+ details += "attached:#" + index + " ";
+ } else {
+ details += "attached:unindexed ";
+ }
+ } else {
+ details += "detached ";
+ }
+
+ details += "sortable:" + sortable + " ";
+
+ return getClass().getSimpleName() + "[" + details.trim() + "]";
+ }
+
+ /**
+ * Sets the minimum width for this column.
+ * <p>
+ * This defines the minimum guaranteed pixel width of the column
+ * <em>when it is set to expand</em>.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param pixels
+ * the minimum width
+ * @return this column
+ */
+ public Column<C, T> setMinimumWidth(double pixels) {
+ final double maxwidth = getMaximumWidth();
+ if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
+ throw new IllegalArgumentException("New minimum width ("
+ + pixels + ") was greater than maximum width ("
+ + maxwidth + ")");
+ }
+
+ if (minimumWidthPx != pixels) {
+ minimumWidthPx = pixels;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ /**
+ * Sets the maximum width for this column.
+ * <p>
+ * This defines the maximum allowed pixel width of the column
+ * <em>when it is set to expand</em>.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param pixels
+ * the maximum width
+ * @param immediately
+ * <code>true</code> if the widths should be executed
+ * immediately (ignoring lazy loading completely), or
+ * <code>false</code> if the command should be run after a
+ * while (duplicate non-immediately invocations are ignored).
+ * @return this column
+ */
+ public Column<C, T> setMaximumWidth(double pixels) {
+ final double minwidth = getMinimumWidth();
+ if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
+ throw new IllegalArgumentException("New maximum width ("
+ + pixels + ") was less than minimum width (" + minwidth
+ + ")");
+ }
+
+ if (maximumWidthPx != pixels) {
+ maximumWidthPx = pixels;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ /**
+ * Sets the ratio with which the column expands.
+ * <p>
+ * By default, all columns expand equally (treated as if all of them had
+ * an expand ratio of 1). Once at least one column gets a defined expand
+ * ratio, the implicit expand ratio is removed, and only the defined
+ * expand ratios are taken into account.
+ * <p>
+ * If a column has a defined width ({@link #setWidth(double)}), it
+ * overrides this method's effects.
+ * <p>
+ * <em>Example:</em> A grid with three columns, with expand ratios 0, 1
+ * and 2, respectively. The column with a <strong>ratio of 0 is exactly
+ * as wide as its contents requires</strong>. The column with a ratio of
+ * 1 is as wide as it needs, <strong>plus a third of any excess
+ * space</strong>, bceause we have 3 parts total, and this column
+ * reservs only one of those. The column with a ratio of 2, is as wide
+ * as it needs to be, <strong>plus two thirds</strong> of the excess
+ * width.
+ * <p>
+ * This action is done "finally", once the current execution loop
+ * returns. This is done to reduce overhead of unintentionally always
+ * recalculate all columns, when modifying several columns at once.
+ *
+ * @param expandRatio
+ * the expand ratio of this column. {@code 0} to not have it
+ * expand at all. A negative number to clear the expand
+ * value.
+ * @return this column
+ */
+ public Column<C, T> setExpandRatio(int ratio) {
+ if (expandRatio != ratio) {
+ expandRatio = ratio;
+ scheduleColumnWidthRecalculator();
+ }
+ return this;
+ }
+
+ /**
+ * Clears the column's expand ratio.
+ * <p>
+ * Same as calling {@link #setExpandRatio(int) setExpandRatio(-1)}
+ *
+ * @return this column
+ */
+ public Column<C, T> clearExpandRatio() {
+ return setExpandRatio(-1);
+ }
+
+ /**
+ * Gets the minimum width for this column.
+ *
+ * @return the minimum width for this column
+ * @see #setMinimumWidth(double)
+ */
+ public double getMinimumWidth() {
+ return minimumWidthPx;
+ }
+
+ /**
+ * Gets the maximum width for this column.
+ *
+ * @return the maximum width for this column
+ * @see #setMaximumWidth(double)
+ */
+ public double getMaximumWidth() {
+ return maximumWidthPx;
+ }
+
+ /**
+ * Gets the expand ratio for this column.
+ *
+ * @return the expand ratio for this column
+ * @see #setExpandRatio(int)
+ */
+ public int getExpandRatio() {
+ return expandRatio;
+ }
+
+ private void scheduleColumnWidthRecalculator() {
+ if (grid != null) {
+ grid.autoColumnWidthsRecalculator.schedule();
+ } else {
+ /*
+ * NOOP
+ *
+ * Since setGrid() will call reapplyWidths as the colum is
+ * attached to a grid, it will call setWidth, which, in turn,
+ * will call this method again. Therefore, it's guaranteed that
+ * the recalculation is scheduled eventually, once the column is
+ * attached to a grid.
+ */
+ }
+ }
+ }
+
+ protected class BodyUpdater implements EscalatorUpdater {
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ int rowIndex = row.getRow();
+ rowReference.set(rowIndex, getDataSource().getRow(rowIndex),
+ row.getElement());
+ for (FlyweightCell cell : cellsToAttach) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof ComplexRenderer) {
+ try {
+ rendererCellReference.set(cell,
+ getColumn(cell.getColumn()));
+ ((ComplexRenderer<?>) renderer)
+ .init(rendererCellReference);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error initing cell in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ for (FlyweightCell cell : attachedCells) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof WidgetRenderer) {
+ try {
+ WidgetRenderer<?, ?> widgetRenderer = (WidgetRenderer<?, ?>) renderer;
+
+ Widget widget = widgetRenderer.createWidget();
+ assert widget != null : "WidgetRenderer.createWidget() returned null. It should return a widget.";
+ assert widget.getParent() == null : "WidgetRenderer.createWidget() returned a widget which already is attached.";
+ assert cell.getElement().getChildCount() == 0 : "Cell content should be empty when adding Widget";
+
+ // Physical attach
+ cell.getElement().appendChild(widget.getElement());
+
+ // Logical attach
+ setParent(widget, Grid.this);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error attaching child widget in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ int rowIndex = row.getRow();
+ TableRowElement rowElement = row.getElement();
+ T rowData = dataSource.getRow(rowIndex);
+
+ boolean hasData = rowData != null;
+
+ /*
+ * TODO could be more efficient to build a list of all styles that
+ * should be used and update the element only once instead of
+ * attempting to update only the ones that have changed.
+ */
+
+ // Assign stylename for rows with data
+ boolean usedToHaveData = rowElement
+ .hasClassName(rowHasDataStyleName);
+
+ if (usedToHaveData != hasData) {
+ setStyleName(rowElement, rowHasDataStyleName, hasData);
+ }
+
+ boolean isEvenIndex = (row.getRow() % 2 == 0);
+ setStyleName(rowElement, rowStripeStyleName, !isEvenIndex);
+
+ rowReference.set(rowIndex, rowData, rowElement);
+
+ if (hasData) {
+ setStyleName(rowElement, rowSelectedStyleName,
+ isSelected(rowData));
+
+ if (rowStyleGenerator != null) {
+ try {
+ String rowStylename = rowStyleGenerator
+ .getStyle(rowReference);
+ setCustomStyleName(rowElement, rowStylename);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error generating styles for row "
+ + row.getRow(), e);
+ }
+ } else {
+ // Remove in case there was a generator previously
+ setCustomStyleName(rowElement, null);
+ }
+ } else if (usedToHaveData) {
+ setStyleName(rowElement, rowSelectedStyleName, false);
+
+ setCustomStyleName(rowElement, null);
+ }
+
+ cellFocusHandler.updateFocusedRowStyle(row);
+
+ for (FlyweightCell cell : cellsToUpdate) {
+ Column<?, T> column = getColumn(cell.getColumn());
+
+ assert column != null : "Column was not found from cell ("
+ + cell.getColumn() + "," + cell.getRow() + ")";
+
+ cellFocusHandler.updateFocusedCellStyle(cell,
+ escalator.getBody());
+
+ if (hasData && cellStyleGenerator != null) {
+ try {
+ cellReference.set(cell.getColumn(), column);
+ String generatedStyle = cellStyleGenerator
+ .getStyle(cellReference);
+ setCustomStyleName(cell.getElement(), generatedStyle);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error generating style for cell in column "
+ + cell.getColumn(), e);
+ }
+ } else if (hasData || usedToHaveData) {
+ setCustomStyleName(cell.getElement(), null);
+ }
+
+ Renderer renderer = column.getRenderer();
+
+ try {
+ rendererCellReference.set(cell, column);
+ if (renderer instanceof ComplexRenderer) {
+ // Hide cell content if needed
+ ComplexRenderer clxRenderer = (ComplexRenderer) renderer;
+ if (hasData) {
+ if (!usedToHaveData) {
+ // Prepare cell for rendering
+ clxRenderer.setContentVisible(
+ rendererCellReference, true);
+ }
+
+ Object value = column.getValue(rowData);
+ clxRenderer.render(rendererCellReference, value);
+
+ } else {
+ // Prepare cell for no data
+ clxRenderer.setContentVisible(
+ rendererCellReference, false);
+ }
+
+ } else if (hasData) {
+ // Simple renderers just render
+ Object value = column.getValue(rowData);
+ renderer.render(rendererCellReference, value);
+
+ } else {
+ // Clear cell if there is no data
+ cell.getElement().removeAllChildren();
+ }
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error rendering cell in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ for (FlyweightCell cell : cellsToDetach) {
+ Renderer renderer = findRenderer(cell);
+ if (renderer instanceof WidgetRenderer) {
+ try {
+ Widget w = WidgetUtil.findWidget(cell.getElement()
+ .getFirstChildElement(), Widget.class);
+ if (w != null) {
+
+ // Logical detach
+ setParent(w, null);
+
+ // Physical detach
+ cell.getElement().removeChild(w.getElement());
+ }
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error detaching widget in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ int rowIndex = row.getRow();
+ // Passing null row data since it might not exist in the data source
+ // any more
+ rowReference.set(rowIndex, null, row.getElement());
+ for (FlyweightCell cell : detachedCells) {
+ Renderer renderer = findRenderer(cell);
+ if (renderer instanceof ComplexRenderer) {
+ try {
+ rendererCellReference.set(cell,
+ getColumn(cell.getColumn()));
+ ((ComplexRenderer) renderer)
+ .destroy(rendererCellReference);
+ } catch (RuntimeException e) {
+ getLogger().log(
+ Level.SEVERE,
+ "Error destroying cell in column "
+ + cell.getColumn(), e);
+ }
+ }
+ }
+ }
+ }
+
+ protected class StaticSectionUpdater implements EscalatorUpdater {
+
+ private StaticSection<?> section;
+ private RowContainer container;
+
+ public StaticSectionUpdater(StaticSection<?> section,
+ RowContainer container) {
+ super();
+ this.section = section;
+ this.container = container;
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ StaticSection.StaticRow<?> staticRow = section.getRow(row.getRow());
+ final List<Column<?, T>> columns = getColumns();
+
+ setCustomStyleName(row.getElement(), staticRow.getStyleName());
+
+ for (FlyweightCell cell : cellsToUpdate) {
+ final StaticSection.StaticCell metadata = staticRow
+ .getCell(columns.get(cell.getColumn()));
+
+ // Decorate default row with sorting indicators
+ if (staticRow instanceof HeaderRow) {
+ addSortingIndicatorsToHeaderRow((HeaderRow) staticRow, cell);
+ }
+
+ // Assign colspan to cell before rendering
+ cell.setColSpan(metadata.getColspan());
+
+ TableCellElement element = cell.getElement();
+ switch (metadata.getType()) {
+ case TEXT:
+ element.setInnerText(metadata.getText());
+ break;
+ case HTML:
+ element.setInnerHTML(metadata.getHtml());
+ break;
+ case WIDGET:
+ preDetach(row, Arrays.asList(cell));
+ element.setInnerHTML("");
+ postAttach(row, Arrays.asList(cell));
+ break;
+ }
+ setCustomStyleName(element, metadata.getStyleName());
+
+ cellFocusHandler.updateFocusedCellStyle(cell, container);
+ }
+ }
+
+ private void addSortingIndicatorsToHeaderRow(HeaderRow headerRow,
+ FlyweightCell cell) {
+
+ cleanup(cell);
+
+ Column<?, ?> column = getColumn(cell.getColumn());
+ SortOrder sortingOrder = getSortOrder(column);
+ if (!headerRow.isDefault() || !column.isSortable()
+ || sortingOrder == null) {
+ // Only apply sorting indicators to sortable header columns in
+ // the default header row
+ return;
+ }
+
+ Element cellElement = cell.getElement();
+
+ if (SortDirection.ASCENDING == sortingOrder.getDirection()) {
+ cellElement.addClassName("sort-asc");
+ } else {
+ cellElement.addClassName("sort-desc");
+ }
+
+ int sortIndex = Grid.this.getSortOrder().indexOf(sortingOrder);
+ if (sortIndex > -1 && Grid.this.getSortOrder().size() > 1) {
+ // Show sort order indicator if column is
+ // sorted and other sorted columns also exists.
+ cellElement.setAttribute("sort-order",
+ String.valueOf(sortIndex + 1));
+ }
+ }
+
+ /**
+ * Finds the sort order for this column
+ */
+ private SortOrder getSortOrder(Column<?, ?> column) {
+ for (SortOrder order : Grid.this.getSortOrder()) {
+ if (order.getColumn() == column) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ private void cleanup(FlyweightCell cell) {
+ Element cellElement = cell.getElement();
+ cellElement.removeAttribute("sort-order");
+ cellElement.removeClassName("sort-desc");
+ cellElement.removeClassName("sort-asc");
+ }
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ StaticSection.StaticRow<?> gridRow = section.getRow(row.getRow());
+ List<Column<?, T>> columns = getColumns();
+
+ for (FlyweightCell cell : attachedCells) {
+ StaticSection.StaticCell metadata = gridRow.getCell(columns
+ .get(cell.getColumn()));
+ /*
+ * If the cell contains widgets that are not currently attach
+ * then attach them now.
+ */
+ if (GridStaticCellType.WIDGET.equals(metadata.getType())) {
+ final Widget widget = metadata.getWidget();
+ final Element cellElement = cell.getElement();
+
+ if (!widget.isAttached()) {
+
+ // Physical attach
+ cellElement.appendChild(widget.getElement());
+
+ // Logical attach
+ setParent(widget, Grid.this);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ if (section.getRowCount() > row.getRow()) {
+ StaticSection.StaticRow<?> gridRow = section.getRow(row
+ .getRow());
+ List<Column<?, T>> columns = getColumns();
+ for (FlyweightCell cell : cellsToDetach) {
+ StaticSection.StaticCell metadata = gridRow.getCell(columns
+ .get(cell.getColumn()));
+
+ if (GridStaticCellType.WIDGET.equals(metadata.getType())
+ && metadata.getWidget().isAttached()) {
+
+ Widget widget = metadata.getWidget();
+
+ // Logical detach
+ setParent(widget, null);
+
+ // Physical detach
+ widget.getElement().removeFromParent();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ }
+ };
+
+ /**
+ * Creates a new instance.
+ */
+ public Grid() {
+ initWidget(escalator);
+ getElement().setTabIndex(0);
+ cellFocusHandler = new CellFocusHandler();
+
+ setStylePrimaryName("v-grid");
+
+ escalator.getHeader().setEscalatorUpdater(createHeaderUpdater());
+ escalator.getBody().setEscalatorUpdater(createBodyUpdater());
+ escalator.getFooter().setEscalatorUpdater(createFooterUpdater());
+
+ header.setGrid(this);
+ HeaderRow defaultRow = header.appendRow();
+ header.setDefaultRow(defaultRow);
+
+ footer.setGrid(this);
+
+ editor.setGrid(this);
+
+ setSelectionMode(SelectionMode.SINGLE);
+
+ escalator.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ fireEvent(new ScrollEvent());
+ }
+ });
+
+ escalator
+ .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() {
+ @Override
+ public void onRowVisibilityChange(
+ RowVisibilityChangeEvent event) {
+ if (dataSource != null && dataSource.size() != 0) {
+ dataIsBeingFetched = true;
+ dataSource.ensureAvailability(
+ event.getFirstVisibleRow(),
+ event.getVisibleRowCount());
+ }
+ }
+ });
+
+ // Default action on SelectionEvents. Refresh the body so changed
+ // become visible.
+ addSelectionHandler(new SelectionHandler<T>() {
+
+ @Override
+ public void onSelect(SelectionEvent<T> event) {
+ refreshBody();
+ }
+ });
+
+ // Sink header events and key events
+ sinkEvents(getHeader().getConsumedEvents());
+ sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP,
+ BrowserEvents.KEYPRESS, BrowserEvents.DBLCLICK,
+ BrowserEvents.MOUSEDOWN));
+
+ // Make ENTER and SHIFT+ENTER in the header perform sorting
+ addHeaderKeyUpHandler(new HeaderKeyUpHandler() {
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ if (event.getNativeKeyCode() != KeyCodes.KEY_ENTER) {
+ return;
+ }
+
+ sorter.sort(event.getFocusedCell().getColumn(),
+ event.isShiftKeyDown());
+ }
+ });
+
+ addDataAvailableHandler(new DataAvailableHandler() {
+ @Override
+ public void onDataAvailable(DataAvailableEvent event) {
+ dataIsBeingFetched = false;
+ }
+ });
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (enabled == this.enabled) {
+ return;
+ }
+
+ this.enabled = enabled;
+ getElement().setTabIndex(enabled ? 0 : -1);
+
+ // Editor save and cancel buttons need to be disabled.
+ boolean editorOpen = editor.getState() != State.INACTIVE;
+ if (editorOpen) {
+ editor.setGridEnabled(enabled);
+ }
+
+ getEscalator().setScrollLocked(Direction.VERTICAL,
+ !enabled || editorOpen);
+ getEscalator().setScrollLocked(Direction.HORIZONTAL, !enabled);
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+ escalator.setStylePrimaryName(style);
+ editor.setStylePrimaryName(style);
+
+ String rowStyle = getStylePrimaryName() + "-row";
+ rowHasDataStyleName = rowStyle + "-has-data";
+ rowSelectedStyleName = rowStyle + "-selected";
+ rowStripeStyleName = rowStyle + "-stripe";
+
+ cellFocusStyleName = getStylePrimaryName() + "-cell-focused";
+ rowFocusStyleName = getStylePrimaryName() + "-row-focused";
+
+ if (isAttached()) {
+ refreshHeader();
+ refreshBody();
+ refreshFooter();
+ }
+ }
+
+ /**
+ * Creates the escalator updater used to update the header rows in this
+ * grid. The updater is invoked when header rows or columns are added or
+ * removed, or the content of existing header cells is changed.
+ *
+ * @return the new header updater instance
+ *
+ * @see GridHeader
+ * @see Grid#getHeader()
+ */
+ protected EscalatorUpdater createHeaderUpdater() {
+ return new StaticSectionUpdater(header, escalator.getHeader());
+ }
+
+ /**
+ * Creates the escalator updater used to update the body rows in this grid.
+ * The updater is invoked when body rows or columns are added or removed,
+ * the content of body cells is changed, or the body is scrolled to expose
+ * previously hidden content.
+ *
+ * @return the new body updater instance
+ */
+ protected EscalatorUpdater createBodyUpdater() {
+ return new BodyUpdater();
+ }
+
+ /**
+ * Creates the escalator updater used to update the footer rows in this
+ * grid. The updater is invoked when header rows or columns are added or
+ * removed, or the content of existing header cells is changed.
+ *
+ * @return the new footer updater instance
+ *
+ * @see GridFooter
+ * @see #getFooter()
+ */
+ protected EscalatorUpdater createFooterUpdater() {
+ return new StaticSectionUpdater(footer, escalator.getFooter());
+ }
+
+ /**
+ * Refreshes header or footer rows on demand
+ *
+ * @param rows
+ * The row container
+ * @param firstRowIsVisible
+ * is the first row visible
+ * @param isHeader
+ * <code>true</code> if we refreshing the header, else assumed
+ * the footer
+ */
+ private void refreshRowContainer(RowContainer rows, StaticSection<?> section) {
+
+ // Add or Remove rows on demand
+ int rowDiff = section.getVisibleRowCount() - rows.getRowCount();
+ if (rowDiff > 0) {
+ rows.insertRows(0, rowDiff);
+ } else if (rowDiff < 0) {
+ rows.removeRows(0, -rowDiff);
+ }
+
+ // Refresh all the rows
+ if (rows.getRowCount() > 0) {
+ rows.refreshRows(0, rows.getRowCount());
+ }
+ }
+
+ /**
+ * Refreshes all header rows
+ */
+ void refreshHeader() {
+ refreshRowContainer(escalator.getHeader(), header);
+ }
+
+ /**
+ * Refreshes all body rows
+ */
+ private void refreshBody() {
+ escalator.getBody().refreshRows(0, escalator.getBody().getRowCount());
+ }
+
+ /**
+ * Refreshes all footer rows
+ */
+ void refreshFooter() {
+ refreshRowContainer(escalator.getFooter(), footer);
+ }
+
+ /**
+ * Adds columns as the last columns in the grid.
+ *
+ * @param columns
+ * the columns to add
+ */
+ public void addColumns(Column<?, T>... columns) {
+ int count = getColumnCount();
+ for (Column<?, T> column : columns) {
+ addColumn(column, count++);
+ }
+ }
+
+ /**
+ * Adds a column as the last column in the grid.
+ *
+ * @param column
+ * the column to add
+ * @return given column
+ */
+ public <C extends Column<?, T>> C addColumn(C column) {
+ addColumn(column, getColumnCount());
+ return column;
+ }
+
+ /**
+ * Inserts a column into a specific position in the grid.
+ *
+ * @param index
+ * the index where the column should be inserted into
+ * @param column
+ * the column to add
+ * @return given column
+ *
+ * @throws IllegalStateException
+ * if Grid's current selection model renders a selection column,
+ * and {@code index} is 0.
+ */
+ public <C extends Column<?, T>> C addColumn(C column, int index) {
+ if (column == selectionColumn) {
+ throw new IllegalArgumentException("The selection column many "
+ + "not be added manually");
+ } else if (selectionColumn != null && index == 0) {
+ throw new IllegalStateException("A column cannot be inserted "
+ + "before the selection column");
+ }
+
+ addColumnSkipSelectionColumnCheck(column, index);
+ return column;
+ }
+
+ private void addColumnSkipSelectionColumnCheck(Column<?, T> column,
+ int index) {
+ // Register column with grid
+ columns.add(index, column);
+
+ header.addColumn(column);
+ footer.addColumn(column);
+
+ // Register this grid instance with the column
+ ((Column<?, T>) column).setGrid(this);
+
+ // Add to escalator
+ escalator.getColumnConfiguration().insertColumns(index, 1);
+
+ // Reapply column width
+ column.reapplyWidth();
+
+ // Sink all renderer events
+ Set<String> events = new HashSet<String>();
+ events.addAll(getConsumedEventsForRenderer(column.getRenderer()));
+
+ sinkEvents(events);
+ }
+
+ private void sinkEvents(Collection<String> events) {
+ assert events != null;
+
+ int eventsToSink = 0;
+ for (String typeName : events) {
+ int typeInt = Event.getTypeInt(typeName);
+ if (typeInt < 0) {
+ // Type not recognized by typeInt
+ sinkBitlessEvent(typeName);
+ } else {
+ eventsToSink |= typeInt;
+ }
+ }
+
+ if (eventsToSink > 0) {
+ sinkEvents(eventsToSink);
+ }
+ }
+
+ private Renderer<?> findRenderer(FlyweightCell cell) {
+ Column<?, T> column = getColumn(cell.getColumn());
+ assert column != null : "Could not find column at index:"
+ + cell.getColumn();
+ return column.getRenderer();
+ }
+
+ /**
+ * Removes a column from the grid.
+ *
+ * @param column
+ * the column to remove
+ */
+ public void removeColumn(Column<?, T> column) {
+ if (column != null && column.equals(selectionColumn)) {
+ throw new IllegalArgumentException(
+ "The selection column may not be removed manually.");
+ }
+
+ removeColumnSkipSelectionColumnCheck(column);
+ }
+
+ private void removeColumnSkipSelectionColumnCheck(Column<?, T> column) {
+ int columnIndex = columns.indexOf(column);
+
+ // Remove from column configuration
+ escalator.getColumnConfiguration().removeColumns(columnIndex, 1);
+
+ updateFrozenColumns();
+
+ header.removeColumn(column);
+ footer.removeColumn(column);
+
+ // de-register column with grid
+ ((Column<?, T>) column).setGrid(null);
+
+ columns.remove(columnIndex);
+ }
+
+ /**
+ * Returns the amount of columns in the grid.
+ *
+ * @return The number of columns in the grid
+ */
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ /**
+ * Returns a list of columns in the grid.
+ *
+ * @return A unmodifiable list of the columns in the grid
+ */
+ public List<Column<?, T>> getColumns() {
+ return Collections
+ .unmodifiableList(new ArrayList<Column<?, T>>(columns));
+ }
+
+ /**
+ * Returns a column by its index in the grid.
+ *
+ * @param index
+ * the index of the column
+ * @return The column in the given index
+ * @throws IllegalArgumentException
+ * if the column index does not exist in the grid
+ */
+ public Column<?, T> getColumn(int index) throws IllegalArgumentException {
+ if (index < 0 || index >= columns.size()) {
+ throw new IllegalStateException("Column not found.");
+ }
+ return columns.get(index);
+ }
+
+ /**
+ * Returns current index of given column
+ *
+ * @param column
+ * column in grid
+ * @return column index, or <code>-1</code> if not in this Grid
+ */
+ protected int indexOfColumn(Column<?, T> column) {
+ return columns.indexOf(column);
+ }
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ protected Header getHeader() {
+ return header;
+ }
+
+ /**
+ * Gets the header row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return header row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public HeaderRow getHeaderRow(int rowIndex) {
+ return header.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the header section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow addHeaderRowAt(int index) {
+ return header.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the header section.
+ *
+ * @return the new row
+ * @see #prependHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow appendHeaderRow() {
+ return header.appendRow();
+ }
+
+ /**
+ * Returns the current default row of the header section. The default row is
+ * a special header row providing a user interface for sorting columns.
+ * Setting a header caption for column updates cells in the default header.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultHeaderRow() {
+ return header.getDefaultRow();
+ }
+
+ /**
+ * Gets the row count for the header section.
+ *
+ * @return row count
+ */
+ public int getHeaderRowCount() {
+ return header.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the header section.
+ *
+ * @return the new row
+ * @see #appendHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow prependHeaderRow() {
+ return header.prependRow();
+ }
+
+ /**
+ * Removes the given row from the header section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeHeaderRow(int)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(HeaderRow row) {
+ header.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the header section.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(int rowIndex) {
+ header.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the default row of the header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * header does not contain the row
+ */
+ public void setDefaultHeaderRow(HeaderRow row) {
+ header.setDefaultRow(row);
+ }
+
+ /**
+ * Sets the visibility of the header section.
+ *
+ * @param visible
+ * true to show header section, false to hide
+ */
+ public void setHeaderVisible(boolean visible) {
+ header.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the header section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isHeaderVisible() {
+ return header.isVisible();
+ }
+
+ /* Grid Footers */
+
+ /**
+ * Returns the footer section of this grid. The default footer is empty.
+ *
+ * @return the footer
+ */
+ protected Footer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Gets the footer row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return footer row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public FooterRow getFooterRow(int rowIndex) {
+ return footer.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the footer section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow addFooterRowAt(int index) {
+ return footer.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the footer section.
+ *
+ * @return the new row
+ * @see #prependFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow appendFooterRow() {
+ return footer.appendRow();
+ }
+
+ /**
+ * Gets the row count for the footer.
+ *
+ * @return row count
+ */
+ public int getFooterRowCount() {
+ return footer.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the footer section.
+ *
+ * @return the new row
+ * @see #appendFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow prependFooterRow() {
+ return footer.prependRow();
+ }
+
+ /**
+ * Removes the given row from the footer section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeFooterRow(int)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(FooterRow row) {
+ footer.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the footer section.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeFooterRow(FooterRow)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(int rowIndex) {
+ footer.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the visibility of the footer section.
+ *
+ * @param visible
+ * true to show footer section, false to hide
+ */
+ public void setFooterVisible(boolean visible) {
+ footer.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the footer section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isFooterVisible() {
+ return footer.isVisible();
+ }
+
+ protected Editor<T> getEditor() {
+ return editor;
+ }
+
+ protected Escalator getEscalator() {
+ return escalator;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(String height) {
+ escalator.setHeight(height);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ escalator.setWidth(width);
+ }
+
+ /**
+ * Sets the data source used by this grid.
+ *
+ * @param dataSource
+ * the data source to use, not null
+ * @throws IllegalArgumentException
+ * if <code>dataSource</code> is <code>null</code>
+ */
+ public void setDataSource(final DataSource<T> dataSource)
+ throws IllegalArgumentException {
+ if (dataSource == null) {
+ throw new IllegalArgumentException("dataSource can't be null.");
+ }
+
+ selectionModel.reset();
+
+ if (this.dataSource != null) {
+ this.dataSource.setDataChangeHandler(null);
+ }
+
+ this.dataSource = dataSource;
+ dataSource.setDataChangeHandler(new DataChangeHandler() {
+ @Override
+ public void dataUpdated(int firstIndex, int numberOfItems) {
+ escalator.getBody().refreshRows(firstIndex, numberOfItems);
+ }
+
+ @Override
+ public void dataRemoved(int firstIndex, int numberOfItems) {
+ escalator.getBody().removeRows(firstIndex, numberOfItems);
+ Range removed = Range.withLength(firstIndex, numberOfItems);
+ cellFocusHandler.rowsRemovedFromBody(removed);
+ }
+
+ @Override
+ public void dataAdded(int firstIndex, int numberOfItems) {
+ escalator.getBody().insertRows(firstIndex, numberOfItems);
+ Range added = Range.withLength(firstIndex, numberOfItems);
+ cellFocusHandler.rowsAddedToBody(added);
+ }
+
+ @Override
+ public void dataAvailable(int firstIndex, int numberOfItems) {
+ currentDataAvailable = Range.withLength(firstIndex,
+ numberOfItems);
+ fireEvent(new DataAvailableEvent(currentDataAvailable));
+ }
+
+ @Override
+ public void resetDataAndSize(int newSize) {
+ RowContainer body = escalator.getBody();
+ int oldSize = body.getRowCount();
+
+ if (newSize > oldSize) {
+ body.insertRows(oldSize, newSize - oldSize);
+ } else if (newSize < oldSize) {
+ body.removeRows(newSize, oldSize - newSize);
+ }
+
+ if (newSize > 0) {
+ dataIsBeingFetched = true;
+ Range visibleRowRange = escalator.getVisibleRowRange();
+ dataSource.ensureAvailability(visibleRowRange.getStart(),
+ visibleRowRange.length());
+ }
+
+ assert body.getRowCount() == newSize;
+ }
+ });
+
+ int previousRowCount = escalator.getBody().getRowCount();
+ if (previousRowCount != 0) {
+ escalator.getBody().removeRows(0, previousRowCount);
+ }
+
+ setEscalatorSizeFromDataSource();
+ }
+
+ private void setEscalatorSizeFromDataSource() {
+ assert escalator.getBody().getRowCount() == 0;
+
+ int size = dataSource.size();
+ if (size == -1 && isAttached()) {
+ // Exact size is not yet known, start with some reasonable guess
+ // just to get an initial backend request going
+ size = getEscalator().getMaxVisibleRowCount();
+ }
+ if (size > 0) {
+ escalator.getBody().insertRows(0, size);
+ }
+ }
+
+ /**
+ * Gets the {@Link DataSource} for this Grid.
+ *
+ * @return the data source used by this grid
+ */
+ public DataSource<T> getDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * Sets the number of frozen columns in this grid. Setting the count to 0
+ * means that no data columns will be frozen, but the built-in selection
+ * checkbox column will still be frozen if it's in use. Setting the count to
+ * -1 will also disable the selection column.
+ * <p>
+ * The default value is 0.
+ *
+ * @param numberOfColumns
+ * the number of columns that should be frozen
+ *
+ * @throws IllegalArgumentException
+ * if the column count is < -1 or > the number of visible
+ * columns
+ */
+ public void setFrozenColumnCount(int numberOfColumns) {
+ if (numberOfColumns < -1 || numberOfColumns > getColumnCount()) {
+ throw new IllegalArgumentException(
+ "count must be between -1 and the current number of columns ("
+ + getColumnCount() + ")");
+ }
+
+ this.frozenColumnCount = numberOfColumns;
+ updateFrozenColumns();
+ }
+
+ private void updateFrozenColumns() {
+ int numberOfColumns = frozenColumnCount;
+
+ if (numberOfColumns == -1) {
+ numberOfColumns = 0;
+ } else if (selectionColumn != null) {
+ numberOfColumns++;
+ }
+
+ escalator.getColumnConfiguration()
+ .setFrozenColumnCount(numberOfColumns);
+
+ }
+
+ /**
+ * Gets the number of frozen columns in this grid. 0 means that no data
+ * columns will be frozen, but the built-in selection checkbox column will
+ * still be frozen if it's in use. -1 means that not even the selection
+ * column is frozen.
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount() {
+ return frozenColumnCount;
+ }
+
+ public HandlerRegistration addRowVisibilityChangeHandler(
+ RowVisibilityChangeHandler handler) {
+ /*
+ * Reusing Escalator's RowVisibilityChangeHandler, since a scroll
+ * concept is too abstract. e.g. the event needs to be re-sent when the
+ * widget is resized.
+ */
+ return escalator.addRowVisibilityChangeHandler(handler);
+ }
+
+ /**
+ * Scrolls to a certain row, using {@link ScrollDestination#ANY}.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @throws IllegalArgumentException
+ * if rowIndex is below zero, or above the maximum value
+ * supported by the data source.
+ */
+ public void scrollToRow(int rowIndex) throws IllegalArgumentException {
+ scrollToRow(rowIndex, ScrollDestination.ANY,
+ GridConstants.DEFAULT_PADDING);
+ }
+
+ /**
+ * Scrolls to a certain row, using user-specified scroll destination.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @param destination
+ * desired destination placement of scrolled-to-row. See
+ * {@link ScrollDestination} for more information.
+ * @throws IllegalArgumentException
+ * if rowIndex is below zero, or above the maximum value
+ * supported by the data source.
+ */
+ public void scrollToRow(int rowIndex, ScrollDestination destination)
+ throws IllegalArgumentException {
+ scrollToRow(rowIndex, destination,
+ destination == ScrollDestination.MIDDLE ? 0
+ : GridConstants.DEFAULT_PADDING);
+ }
+
+ /**
+ * Scrolls to a certain row using only user-specified parameters.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @param destination
+ * desired destination placement of scrolled-to-row. See
+ * {@link ScrollDestination} for more information.
+ * @param paddingPx
+ * number of pixels to overscroll. Behavior depends on
+ * destination.
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero, because having a padding on a
+ * centered row is undefined behavior, or if rowIndex is below
+ * zero or above the row count of the data source.
+ */
+ private void scrollToRow(int rowIndex, ScrollDestination destination,
+ int paddingPx) throws IllegalArgumentException {
+ int maxsize = escalator.getBody().getRowCount() - 1;
+
+ if (rowIndex < 0) {
+ throw new IllegalArgumentException("Row index (" + rowIndex
+ + ") is below zero!");
+ }
+
+ if (rowIndex > maxsize) {
+ throw new IllegalArgumentException("Row index (" + rowIndex
+ + ") is above maximum (" + maxsize + ")!");
+ }
+
+ escalator.scrollToRow(rowIndex, destination, paddingPx);
+ }
+
+ /**
+ * Scrolls to the beginning of the very first row.
+ */
+ public void scrollToStart() {
+ scrollToRow(0, ScrollDestination.START);
+ }
+
+ /**
+ * Scrolls to the end of the very last row.
+ */
+ public void scrollToEnd() {
+ scrollToRow(escalator.getBody().getRowCount() - 1,
+ ScrollDestination.END);
+ }
+
+ /**
+ * Sets the vertical scroll offset.
+ *
+ * @param px
+ * the number of pixels this grid should be scrolled down
+ */
+ public void setScrollTop(double px) {
+ escalator.setScrollTop(px);
+ }
+
+ /**
+ * Gets the vertical scroll offset
+ *
+ * @return the number of pixels this grid is scrolled down
+ */
+ public double getScrollTop() {
+ return escalator.getScrollTop();
+ }
+
+ /**
+ * Gets the horizontal scroll offset
+ *
+ * @return the number of pixels this grid is scrolled to the right
+ */
+ public double getScrollLeft() {
+ return escalator.getScrollLeft();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead.
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInifinite(double)
+ * infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ public void setHeightByRows(double rows) throws IllegalArgumentException {
+ escalator.setHeightByRows(rows);
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * By default, it is {@value Escalator#DEFAULT_HEIGHT_BY_ROWS}.
+ *
+ * @return the amount of rows that should be shown in Grid's body, while in
+ * {@link HeightMode#ROW}.
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return escalator.getHeightByRows();
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via {@link #setHeight(String)}, and behave as a traditional Widget.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ escalator.setHeightMode(heightMode);
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return escalator.getHeightMode();
+ }
+
+ private Set<String> getConsumedEventsForRenderer(Renderer<?> renderer) {
+ Set<String> events = new HashSet<String>();
+ if (renderer instanceof ComplexRenderer) {
+ Collection<String> consumedEvents = ((ComplexRenderer<?>) renderer)
+ .getConsumedEvents();
+ if (consumedEvents != null) {
+ events.addAll(consumedEvents);
+ }
+ }
+ return events;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (!isEnabled()) {
+ return;
+ }
+
+ EventTarget target = event.getEventTarget();
+
+ if (!Element.is(target)) {
+ return;
+ }
+
+ Element e = Element.as(target);
+ RowContainer container = escalator.findRowContainer(e);
+ Cell cell;
+
+ String eventType = event.getType();
+ if (container == null) {
+ if (eventType.equals(BrowserEvents.KEYDOWN)
+ || eventType.equals(BrowserEvents.KEYUP)
+ || eventType.equals(BrowserEvents.KEYPRESS)) {
+ cell = cellFocusHandler.getFocusedCell();
+ container = cellFocusHandler.containerWithFocus;
+ } else {
+ // Click in a location that does not contain cells.
+ return;
+ }
+ } else {
+ cell = container.getCell(e);
+ if (eventType.equals(BrowserEvents.MOUSEDOWN)) {
+ cellOnPrevMouseDown = cell;
+ } else if (cell == null && eventType.equals(BrowserEvents.CLICK)) {
+ /*
+ * Chrome has an interesting idea on click targets (see
+ * cellOnPrevMouseDown javadoc). Firefox, on the other hand, has
+ * the mousedown target as the click target.
+ */
+ cell = cellOnPrevMouseDown;
+ }
+ }
+
+ assert cell != null : "received " + eventType
+ + "-event with a null cell target";
+ eventCell.set(cell);
+
+ // Editor can steal focus from Grid and is still handled
+ if (handleEditorEvent(event, container)) {
+ return;
+ }
+
+ // Fire GridKeyEvents and GridClickEvents. Pass the event to escalator.
+ super.onBrowserEvent(event);
+
+ if (!isElementInChildWidget(e)) {
+
+ // Sorting through header Click / KeyUp
+ if (handleHeaderDefaultRowEvent(event, container)) {
+ return;
+ }
+
+ if (handleRendererEvent(event, container)) {
+ return;
+ }
+
+ if (handleNavigationEvent(event, container)) {
+ return;
+ }
+
+ if (handleCellFocusEvent(event, container)) {
+ return;
+ }
+ }
+ }
+
+ private boolean isElementInChildWidget(Element e) {
+ Widget w = WidgetUtil.findWidget(e, null);
+
+ if (w == this) {
+ return false;
+ }
+
+ /*
+ * If e is directly inside this grid, but the grid is wrapped in a
+ * Composite, findWidget is not going to find this, only the wrapper.
+ * Thus we need to check its parents to see if we encounter this; if we
+ * don't, the found widget is actually a parent of this, so we should
+ * return false.
+ */
+ while (w != null && w != this) {
+ w = w.getParent();
+ }
+ return w != null;
+ }
+
+ private boolean handleEditorEvent(Event event, RowContainer container) {
+
+ if (editor.getState() != Editor.State.INACTIVE) {
+ if (event.getTypeInt() == Event.ONKEYDOWN
+ && event.getKeyCode() == Editor.KEYCODE_HIDE) {
+ editor.cancel();
+ }
+ return true;
+ }
+
+ if (container == escalator.getBody() && editor.isEnabled()) {
+ if (event.getTypeInt() == Event.ONDBLCLICK) {
+ editor.editRow(eventCell.getRowIndex());
+ return true;
+ } else if (event.getTypeInt() == Event.ONKEYDOWN
+ && event.getKeyCode() == Editor.KEYCODE_SHOW) {
+ editor.editRow(cellFocusHandler.rowWithFocus);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean handleRendererEvent(Event event, RowContainer container) {
+
+ if (container == escalator.getBody()) {
+ Column<?, T> gridColumn = eventCell.getColumn();
+ boolean enterKey = event.getType().equals(BrowserEvents.KEYDOWN)
+ && event.getKeyCode() == KeyCodes.KEY_ENTER;
+ boolean doubleClick = event.getType()
+ .equals(BrowserEvents.DBLCLICK);
+
+ if (gridColumn.getRenderer() instanceof ComplexRenderer) {
+ ComplexRenderer<?> cplxRenderer = (ComplexRenderer<?>) gridColumn
+ .getRenderer();
+ if (cplxRenderer.getConsumedEvents().contains(event.getType())) {
+ if (cplxRenderer.onBrowserEvent(eventCell, event)) {
+ return true;
+ }
+ }
+
+ // Calls onActivate if KeyDown and Enter or double click
+ if ((enterKey || doubleClick)
+ && cplxRenderer.onActivate(eventCell)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean handleCellFocusEvent(Event event, RowContainer container) {
+ Collection<String> navigation = cellFocusHandler.getNavigationEvents();
+ if (navigation.contains(event.getType())) {
+ cellFocusHandler.handleNavigationEvent(event, eventCell);
+ }
+ return false;
+ }
+
+ private boolean handleNavigationEvent(Event event, RowContainer unused) {
+ if (!event.getType().equals(BrowserEvents.KEYDOWN)) {
+ // Only handle key downs
+ return false;
+ }
+
+ int newRow = -1;
+ RowContainer container = escalator.getBody();
+ switch (event.getKeyCode()) {
+ case KeyCodes.KEY_HOME:
+ if (container.getRowCount() > 0) {
+ newRow = 0;
+ }
+ break;
+ case KeyCodes.KEY_END:
+ if (container.getRowCount() > 0) {
+ newRow = container.getRowCount() - 1;
+ }
+ break;
+ case KeyCodes.KEY_PAGEUP: {
+ Range range = escalator.getVisibleRowRange();
+ if (!range.isEmpty()) {
+ int firstIndex = getFirstVisibleRowIndex();
+ newRow = firstIndex - range.length();
+ if (newRow < 0) {
+ newRow = 0;
+ }
+ }
+ break;
+ }
+ case KeyCodes.KEY_PAGEDOWN: {
+ Range range = escalator.getVisibleRowRange();
+ if (!range.isEmpty()) {
+ int lastIndex = getLastVisibleRowIndex();
+ newRow = lastIndex + range.length();
+ if (newRow >= container.getRowCount()) {
+ newRow = container.getRowCount() - 1;
+ }
+ }
+ break;
+ }
+ default:
+ return false;
+ }
+
+ scrollToRow(newRow);
+
+ return true;
+ }
+
+ private Point rowEventTouchStartingPoint;
+ private CellStyleGenerator<T> cellStyleGenerator;
+ private RowStyleGenerator<T> rowStyleGenerator;
+ private RowReference<T> rowReference = new RowReference<T>(this);
+ private CellReference<T> cellReference = new CellReference<T>(rowReference);
+ private RendererCellReference rendererCellReference = new RendererCellReference(
+ (RowReference<Object>) rowReference);
+
+ private boolean handleHeaderDefaultRowEvent(Event event,
+ RowContainer container) {
+ if (container != escalator.getHeader()) {
+ return false;
+ }
+ if (!getHeader().getRow(eventCell.getRowIndex()).isDefault()) {
+ return false;
+ }
+ if (!eventCell.getColumn().isSortable()) {
+ // Only handle sorting events if the column is sortable
+ return false;
+ }
+
+ if (BrowserEvents.MOUSEDOWN.equals(event.getType())
+ && event.getShiftKey()) {
+ // Don't select text when shift clicking on a header.
+ event.preventDefault();
+ }
+
+ if (BrowserEvents.TOUCHSTART.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ Touch touch = event.getChangedTouches().get(0);
+ rowEventTouchStartingPoint = new Point(touch.getClientX(),
+ touch.getClientY());
+
+ sorter.sortAfterDelay(GridConstants.LONG_TAP_DELAY, true);
+
+ return true;
+
+ } else if (BrowserEvents.TOUCHMOVE.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ Touch touch = event.getChangedTouches().get(0);
+ double diffX = Math.abs(touch.getClientX()
+ - rowEventTouchStartingPoint.getX());
+ double diffY = Math.abs(touch.getClientY()
+ - rowEventTouchStartingPoint.getY());
+
+ // Cancel long tap if finger strays too far from
+ // starting point
+ if (diffX > GridConstants.LONG_TAP_THRESHOLD
+ || diffY > GridConstants.LONG_TAP_THRESHOLD) {
+ sorter.cancelDelayedSort();
+ }
+
+ return true;
+
+ } else if (BrowserEvents.TOUCHEND.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ if (sorter.isDelayedSortScheduled()) {
+ // Not a long tap yet, perform single sort
+ sorter.cancelDelayedSort();
+ sorter.sort(eventCell.getColumn(), false);
+ }
+
+ return true;
+
+ } else if (BrowserEvents.TOUCHCANCEL.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ sorter.cancelDelayedSort();
+
+ return true;
+
+ } else if (BrowserEvents.CLICK.equals(event.getType())) {
+
+ sorter.sort(eventCell.getColumn(), event.getShiftKey());
+
+ // Click events should go onward to cell focus logic
+ return false;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
+ // Parse SubPart string to type and indices
+ String[] splitArgs = subPart.split("\\[");
+
+ String type = splitArgs[0];
+ int[] indices = new int[splitArgs.length - 1];
+ for (int i = 0; i < indices.length; ++i) {
+ String tmp = splitArgs[i + 1];
+ indices[i] = Integer.parseInt(tmp.substring(0, tmp.length() - 1));
+ }
+
+ // Get correct RowContainer for type from Escalator
+ RowContainer container = null;
+ if (type.equalsIgnoreCase("header")) {
+ container = escalator.getHeader();
+ } else if (type.equalsIgnoreCase("cell")) {
+ // If wanted row is not visible, we need to scroll there.
+ Range visibleRowRange = escalator.getVisibleRowRange();
+ if (indices.length > 0 && !visibleRowRange.contains(indices[0])) {
+ try {
+ scrollToRow(indices[0]);
+ } catch (IllegalArgumentException e) {
+ getLogger().log(Level.SEVERE, e.getMessage());
+ }
+ // Scrolling causes a lazy loading event. No element can
+ // currently be retrieved.
+ return null;
+ }
+ container = escalator.getBody();
+ } else if (type.equalsIgnoreCase("footer")) {
+ container = escalator.getFooter();
+ } else if (type.equalsIgnoreCase("editor")) {
+ if (editor.getState() != State.ACTIVE) {
+ // Editor is not there.
+ return null;
+ }
+
+ if (indices.length == 0) {
+ return DOM.asOld(editor.editorOverlay);
+ } else if (indices.length == 1 && indices[0] < columns.size()) {
+ escalator.scrollToColumn(indices[0], ScrollDestination.ANY, 0);
+ return editor.getWidget(columns.get(indices[0])).getElement();
+ } else {
+ return null;
+ }
+ }
+
+ if (null != container) {
+ if (indices.length == 0) {
+ // No indexing. Just return the wanted container element
+ return DOM.asOld(container.getElement());
+ } else {
+ try {
+ return DOM.asOld(getSubPart(container, indices));
+ } catch (Exception e) {
+ getLogger().log(Level.SEVERE, e.getMessage());
+ }
+ }
+ }
+ return null;
+ }
+
+ private Element getSubPart(RowContainer container, int[] indices) {
+ Element targetElement = container.getRowElement(indices[0]);
+
+ // Scroll wanted column to view if able
+ if (indices.length > 1 && targetElement != null) {
+ if (escalator.getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
+ escalator.scrollToColumn(indices[1], ScrollDestination.ANY, 0);
+ }
+
+ targetElement = getCellFromRow(TableRowElement.as(targetElement),
+ indices[1]);
+
+ for (int i = 2; i < indices.length && targetElement != null; ++i) {
+ targetElement = (Element) targetElement.getChild(indices[i]);
+ }
+ }
+
+ return targetElement;
+ }
+
+ private Element getCellFromRow(TableRowElement rowElement, int index) {
+ int childCount = rowElement.getCells().getLength();
+ if (index < 0 || index >= childCount) {
+ return null;
+ }
+
+ TableCellElement currentCell = null;
+ boolean indexInColspan = false;
+ int i = 0;
+
+ while (!indexInColspan) {
+ currentCell = rowElement.getCells().getItem(i);
+
+ // Calculate if this is the cell we are looking for
+ int colSpan = currentCell.getColSpan();
+ indexInColspan = index < colSpan + i;
+
+ // Increment by colspan to skip over hidden cells
+ i += colSpan;
+ }
+ return currentCell;
+ }
+
+ @Override
+ public String getSubPartName(com.google.gwt.user.client.Element subElement) {
+ // Containers and matching SubPart types
+ List<RowContainer> containers = Arrays.asList(escalator.getHeader(),
+ escalator.getBody(), escalator.getFooter());
+ List<String> containerType = Arrays.asList("header", "cell", "footer");
+
+ for (int i = 0; i < containers.size(); ++i) {
+ RowContainer container = containers.get(i);
+ boolean containerRow = (subElement.getTagName().equalsIgnoreCase(
+ "tr") && subElement.getParentElement() == container
+ .getElement());
+ if (containerRow) {
+ // Wanted SubPart is row that is a child of containers root
+ // To get indices, we use a cell that is a child of this row
+ subElement = DOM.asOld(subElement.getFirstChildElement());
+ }
+
+ Cell cell = container.getCell(subElement);
+ if (cell != null) {
+ // Skip the column index if subElement was a child of root
+ return containerType.get(i) + "[" + cell.getRow()
+ + (containerRow ? "]" : "][" + cell.getColumn() + "]");
+ }
+ }
+
+ // Check if subelement is part of editor.
+ if (editor.getState() == State.ACTIVE) {
+ if (editor.editorOverlay.isOrHasChild(subElement)) {
+ int i = 0;
+ for (Column<?, T> column : columns) {
+ if (editor.getWidget(column).getElement()
+ .isOrHasChild(subElement)) {
+ return "editor[" + i + "]";
+ }
+ ++i;
+ }
+ return "editor";
+ }
+ }
+
+ return null;
+ }
+
+ private void setSelectColumnRenderer(
+ final Renderer<Boolean> selectColumnRenderer) {
+ if (this.selectColumnRenderer == selectColumnRenderer) {
+ return;
+ }
+
+ if (this.selectColumnRenderer != null) {
+ if (this.selectColumnRenderer instanceof ComplexRenderer) {
+ // End of Life for the old selection column renderer.
+ ((ComplexRenderer<?>) this.selectColumnRenderer).destroy();
+ }
+
+ // Clear field so frozen column logic in the remove method knows
+ // what to do
+ Column<?, T> colToRemove = selectionColumn;
+ selectionColumn = null;
+ removeColumnSkipSelectionColumnCheck(colToRemove);
+ cellFocusHandler.offsetRangeBy(-1);
+ }
+
+ this.selectColumnRenderer = selectColumnRenderer;
+
+ if (selectColumnRenderer != null) {
+ cellFocusHandler.offsetRangeBy(1);
+ selectionColumn = new SelectionColumn(selectColumnRenderer);
+
+ addColumnSkipSelectionColumnCheck(selectionColumn, 0);
+ selectionColumn.initDone();
+ } else {
+ selectionColumn = null;
+ refreshBody();
+ }
+
+ updateFrozenColumns();
+ }
+
+ /**
+ * Sets the current selection model.
+ * <p>
+ * This function will call {@link SelectionModel#setGrid(Grid)}.
+ *
+ * @param selectionModel
+ * a selection model implementation.
+ * @throws IllegalArgumentException
+ * if selection model argument is null
+ */
+ public void setSelectionModel(SelectionModel<T> selectionModel) {
+
+ if (selectionModel == null) {
+ throw new IllegalArgumentException("Selection model can't be null");
+ }
+
+ if (this.selectionModel != null) {
+ // Detach selection model from Grid.
+ this.selectionModel.setGrid(null);
+ }
+
+ this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
+ setSelectColumnRenderer(this.selectionModel
+ .getSelectionColumnRenderer());
+ }
+
+ /**
+ * Gets a reference to the current selection model.
+ *
+ * @return the currently used SelectionModel instance.
+ */
+ public SelectionModel<T> getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets current selection mode.
+ * <p>
+ * This is a shorthand method for {@link Grid#setSelectionModel}.
+ *
+ * @param mode
+ * a selection mode value
+ * @see {@link SelectionMode}.
+ */
+ public void setSelectionMode(SelectionMode mode) {
+ SelectionModel<T> model = mode.createModel();
+ setSelectionModel(model);
+ }
+
+ /**
+ * Test if a row is selected.
+ *
+ * @param row
+ * a row object
+ * @return true, if the current selection model considers the provided row
+ * object selected.
+ */
+ public boolean isSelected(T row) {
+ return selectionModel.isSelected(row);
+ }
+
+ /**
+ * Select a row using the current selection model.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} and
+ * {@link SelectionModel.Multi} are supported; for anything else, an
+ * exception will be thrown.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ @SuppressWarnings("unchecked")
+ public boolean select(T row) {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).select(row);
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel).select(row);
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Deselect a row using the current selection model.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} and
+ * {@link SelectionModel.Multi} are supported; for anything else, an
+ * exception will be thrown.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ @SuppressWarnings("unchecked")
+ public boolean deselect(T row) {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).deselect(row);
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel).deselect(row);
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Gets last selected row from the current SelectionModel.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} are
+ * valid for this method; for anything else, use the
+ * {@link Grid#getSelectedRows()} method.
+ *
+ * @return a selected row reference, or null, if no row is selected
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single}
+ */
+ public T getSelectedRow() {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).getSelectedRow();
+ } else {
+ throw new IllegalStateException(
+ "Unsupported selection model; can not get single selected row");
+ }
+ }
+
+ /**
+ * Gets currently selected rows from the current selection model.
+ *
+ * @return a non-null collection containing all currently selected rows.
+ */
+ public Collection<T> getSelectedRows() {
+ return selectionModel.getSelectedRows();
+ }
+
+ @Override
+ public HandlerRegistration addSelectionHandler(
+ final SelectionHandler<T> handler) {
+ return addHandler(handler, SelectionEvent.getType());
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ *
+ * @param s
+ * a sort instance
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sorts the Grid data in ascending order along one column.
+ *
+ * @param column
+ * a grid column reference
+ */
+ public <C> void sort(Column<C, T> column) {
+ sort(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sorts the Grid data along one column.
+ *
+ * @param column
+ * a grid column reference
+ * @param direction
+ * a sort direction value
+ */
+ public <C> void sort(Column<C, T> column, SortDirection direction) {
+ sort(Sort.by(column, direction));
+ }
+
+ /**
+ * Sets the sort order to use. Setting this causes the Grid to re-sort
+ * itself.
+ *
+ * @param order
+ * a sort order list. If set to null, the sort order is cleared.
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, false);
+ }
+
+ private void setSortOrder(List<SortOrder> order, boolean userOriginated) {
+ if (order != sortOrder) {
+ sortOrder.clear();
+ if (order != null) {
+ sortOrder.addAll(order);
+ }
+ }
+ sort(userOriginated);
+ }
+
+ /**
+ * Get a copy of the current sort order array.
+ *
+ * @return a copy of the current sort order array
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Finds the sorting order for this column
+ */
+ private SortOrder getSortOrder(Column<?, ?> column) {
+ for (SortOrder order : getSortOrder()) {
+ if (order.getColumn() == column) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Register a GWT event handler for a sorting event. This handler gets
+ * called whenever this Grid needs its data source to provide data sorted in
+ * a specific order.
+ *
+ * @param handler
+ * a sort event handler
+ * @return the registration for the event
+ */
+ public HandlerRegistration addSortHandler(SortHandler<T> handler) {
+ return addHandler(handler, SortEvent.getType());
+ }
+
+ /**
+ * Register a GWT event handler for a select all event. This handler gets
+ * called whenever Grid needs all rows selected.
+ *
+ * @param handler
+ * a select all event handler
+ */
+ public HandlerRegistration addSelectAllHandler(SelectAllHandler<T> handler) {
+ return addHandler(handler, SelectAllEvent.getType());
+ }
+
+ /**
+ * Register a GWT event handler for a data available event. This handler
+ * gets called whenever the {@link DataSource} for this Grid has new data
+ * available.
+ * <p>
+ * This handle will be fired with the current available data after
+ * registration is done.
+ *
+ * @param handler
+ * a data available event handler
+ * @return the registartion for the event
+ */
+ public HandlerRegistration addDataAvailableHandler(
+ final DataAvailableHandler handler) {
+ // Deferred call to handler with current row range
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (!dataIsBeingFetched) {
+ handler.onDataAvailable(new DataAvailableEvent(
+ currentDataAvailable));
+ }
+ }
+ });
+ return addHandler(handler, DataAvailableEvent.TYPE);
+ }
+
+ /**
+ * Register a BodyKeyDownHandler to this Grid. The event for this handler is
+ * fired when a KeyDown event occurs while cell focus is in the Body of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyKeyDownHandler(BodyKeyDownHandler handler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyKeyUpHandler to this Grid. The event for this handler is
+ * fired when a KeyUp event occurs while cell focus is in the Body of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyKeyUpHandler(BodyKeyUpHandler handler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyKeyPressHandler to this Grid. The event for this handler
+ * is fired when a KeyPress event occurs while cell focus is in the Body of
+ * this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyKeyPressHandler(
+ BodyKeyPressHandler handler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderKeyDownHandler to this Grid. The event for this handler
+ * is fired when a KeyDown event occurs while cell focus is in the Header of
+ * this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderKeyDownHandler(
+ HeaderKeyDownHandler handler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderKeyUpHandler to this Grid. The event for this handler is
+ * fired when a KeyUp event occurs while cell focus is in the Header of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderKeyUpHandler(HeaderKeyUpHandler handler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderKeyPressHandler to this Grid. The event for this handler
+ * is fired when a KeyPress event occurs while cell focus is in the Header
+ * of this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderKeyPressHandler(
+ HeaderKeyPressHandler handler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterKeyDownHandler to this Grid. The event for this handler
+ * is fired when a KeyDown event occurs while cell focus is in the Footer of
+ * this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterKeyDownHandler(
+ FooterKeyDownHandler handler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterKeyUpHandler to this Grid. The event for this handler is
+ * fired when a KeyUp event occurs while cell focus is in the Footer of this
+ * Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterKeyUpHandler(FooterKeyUpHandler handler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterKeyPressHandler to this Grid. The event for this handler
+ * is fired when a KeyPress event occurs while cell focus is in the Footer
+ * of this Grid.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterKeyPressHandler(
+ FooterKeyPressHandler handler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyClickHandler to this Grid. The event for this handler is
+ * fired when a Click event occurs in the Body of this Grid.
+ *
+ * @param handler
+ * the click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyClickHandler(BodyClickHandler handler) {
+ return addHandler(handler, clickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderClickHandler to this Grid. The event for this handler is
+ * fired when a Click event occurs in the Header of this Grid.
+ *
+ * @param handler
+ * the click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderClickHandler(HeaderClickHandler handler) {
+ return addHandler(handler, clickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterClickHandler to this Grid. The event for this handler is
+ * fired when a Click event occurs in the Footer of this Grid.
+ *
+ * @param handler
+ * the click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterClickHandler(FooterClickHandler handler) {
+ return addHandler(handler, clickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a BodyDoubleClickHandler to this Grid. The event for this
+ * handler is fired when a double click event occurs in the Body of this
+ * Grid.
+ *
+ * @param handler
+ * the double click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addBodyDoubleClickHandler(
+ BodyDoubleClickHandler handler) {
+ return addHandler(handler, doubleClickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a HeaderDoubleClickHandler to this Grid. The event for this
+ * handler is fired when a double click event occurs in the Header of this
+ * Grid.
+ *
+ * @param handler
+ * the double click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addHeaderDoubleClickHandler(
+ HeaderDoubleClickHandler handler) {
+ return addHandler(handler, doubleClickEvent.getAssociatedType());
+ }
+
+ /**
+ * Register a FooterDoubleClickHandler to this Grid. The event for this
+ * handler is fired when a double click event occurs in the Footer of this
+ * Grid.
+ *
+ * @param handler
+ * the double click handler to register
+ * @return the registration for the event
+ */
+ public HandlerRegistration addFooterDoubleClickHandler(
+ FooterDoubleClickHandler handler) {
+ return addHandler(handler, doubleClickEvent.getAssociatedType());
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(boolean userOriginated) {
+ refreshHeader();
+ fireEvent(new SortEvent<T>(this,
+ Collections.unmodifiableList(sortOrder), userOriginated));
+ }
+
+ private int getLastVisibleRowIndex() {
+ int lastRowIndex = escalator.getVisibleRowRange().getEnd();
+ int footerTop = escalator.getFooter().getElement().getAbsoluteTop();
+ Element lastRow;
+
+ do {
+ lastRow = escalator.getBody().getRowElement(--lastRowIndex);
+ } while (lastRow.getAbsoluteTop() > footerTop);
+
+ return lastRowIndex;
+ }
+
+ private int getFirstVisibleRowIndex() {
+ int firstRowIndex = escalator.getVisibleRowRange().getStart();
+ int headerBottom = escalator.getHeader().getElement()
+ .getAbsoluteBottom();
+ Element firstRow = escalator.getBody().getRowElement(firstRowIndex);
+
+ while (firstRow.getAbsoluteBottom() < headerBottom) {
+ firstRow = escalator.getBody().getRowElement(++firstRowIndex);
+ }
+
+ return firstRowIndex;
+ }
+
+ /**
+ * Adds a scroll handler to this grid
+ *
+ * @param handler
+ * the scroll handler to add
+ * @return a handler registration for the registered scroll handler
+ */
+ public HandlerRegistration addScrollHandler(ScrollHandler handler) {
+ return addHandler(handler, ScrollEvent.TYPE);
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return escalator.isWorkPending() || dataIsBeingFetched
+ || autoColumnWidthsRecalculator.isScheduled();
+ }
+
+ /**
+ * Sets a new column order for the grid. All columns which are not ordered
+ * here will remain in the order they were before as the last columns of
+ * grid.
+ *
+ * @param orderedColumns
+ * array of columns in wanted order
+ */
+ public void setColumnOrder(Column<?, T>... orderedColumns) {
+ ColumnConfiguration conf = getEscalator().getColumnConfiguration();
+
+ // Trigger ComplexRenderer.destroy for old content
+ conf.removeColumns(0, conf.getColumnCount());
+
+ List<Column<?, T>> newOrder = new ArrayList<Column<?, T>>();
+ if (selectionColumn != null) {
+ newOrder.add(selectionColumn);
+ }
+
+ int i = 0;
+ for (Column<?, T> column : orderedColumns) {
+ if (columns.contains(column)) {
+ newOrder.add(column);
+ ++i;
+ } else {
+ throw new IllegalArgumentException("Given column at index " + i
+ + " does not exist in Grid");
+ }
+ }
+
+ if (columns.size() != newOrder.size()) {
+ columns.removeAll(newOrder);
+ newOrder.addAll(columns);
+ }
+ columns = newOrder;
+
+ // Do ComplexRenderer.init and render new content
+ conf.insertColumns(0, columns.size());
+
+ // Update column widths.
+ for (Column<?, T> column : columns) {
+ column.reapplyWidth();
+ }
+
+ // Recalculate all the colspans
+ for (HeaderRow row : header.getRows()) {
+ row.calculateColspans();
+ }
+ for (FooterRow row : footer.getRows()) {
+ row.calculateColspans();
+ }
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for cells
+ *
+ * @param cellStyleGenerator
+ * the cell style generator to set, or <code>null</code> to
+ * remove a previously set generator
+ */
+ public void setCellStyleGenerator(CellStyleGenerator<T> cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ refreshBody();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for cells
+ *
+ * @return the cell style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public CellStyleGenerator<T> getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for rows
+ *
+ * @param rowStyleGenerator
+ * the row style generator to set, or <code>null</code> to remove
+ * a previously set generator
+ */
+ public void setRowStyleGenerator(RowStyleGenerator<T> rowStyleGenerator) {
+ this.rowStyleGenerator = rowStyleGenerator;
+ refreshBody();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for rows
+ *
+ * @return the row style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public RowStyleGenerator<T> getRowStyleGenerator() {
+ return rowStyleGenerator;
+ }
+
+ private static void setCustomStyleName(Element element, String styleName) {
+ assert element != null;
+
+ String oldStyleName = element
+ .getPropertyString(CUSTOM_STYLE_PROPERTY_NAME);
+
+ if (!SharedUtil.equals(oldStyleName, styleName)) {
+ if (oldStyleName != null) {
+ element.removeClassName(oldStyleName);
+ }
+ if (styleName != null) {
+ element.addClassName(styleName);
+ }
+ element.setPropertyString(CUSTOM_STYLE_PROPERTY_NAME, styleName);
+ }
+
+ }
+
+ /**
+ * Opens the editor over the row with the given index.
+ *
+ * @param rowIndex
+ * the index of the row to be edited
+ *
+ * @throws IllegalStateException
+ * if the editor is not enabled
+ * @throws IllegalStateException
+ * if the editor is already in edit mode
+ */
+ public void editRow(int rowIndex) {
+ editor.editRow(rowIndex);
+ }
+
+ /**
+ * Saves any unsaved changes in the editor to the data source.
+ *
+ * @throws IllegalStateException
+ * if the editor is not enabled
+ * @throws IllegalStateException
+ * if the editor is not in edit mode
+ */
+ public void saveEditor() {
+ editor.save();
+ }
+
+ /**
+ * Cancels the currently active edit and hides the editor. Any changes that
+ * are not {@link #saveEditor() saved} are lost.
+ *
+ * @throws IllegalStateException
+ * if the editor is not enabled
+ * @throws IllegalStateException
+ * if the editor is not in edit mode
+ */
+ public void cancelEditor() {
+ editor.cancel();
+ }
+
+ /**
+ * Returns the handler responsible for binding data and editor widgets to
+ * the editor.
+ *
+ * @return the editor handler or null if not set
+ */
+ public EditorHandler<T> getEditorHandler() {
+ return editor.getHandler();
+ }
+
+ /**
+ * Sets the handler responsible for binding data and editor widgets to the
+ * editor.
+ *
+ * @param rowHandler
+ * the new editor handler
+ *
+ * @throws IllegalStateException
+ * if the editor is currently in edit mode
+ */
+ public void setEditorHandler(EditorHandler<T> handler) {
+ editor.setHandler(handler);
+ }
+
+ /**
+ * Returns the enabled state of the editor.
+ *
+ * @return true if editing is enabled, false otherwise
+ */
+ public boolean isEditorEnabled() {
+ return editor.isEnabled();
+ }
+
+ /**
+ * Sets the enabled state of the editor.
+ *
+ * @param enabled
+ * true to enable editing, false to disable
+ *
+ * @throws IllegalStateException
+ * if in edit mode and trying to disable
+ * @throws IllegalStateException
+ * if the editor handler is not set
+ */
+ public void setEditorEnabled(boolean enabled) {
+ editor.setEnabled(enabled);
+ }
+
+ /**
+ * Returns the editor widget associated with the given column. If the editor
+ * is not active, returns null.
+ *
+ * @param column
+ * the column
+ * @return the widget if the editor is open, null otherwise
+ */
+ public Widget getEditorWidget(Column<?, T> column) {
+ return editor.getWidget(column);
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+
+ if (getEscalator().getBody().getRowCount() == 0 && dataSource != null) {
+ setEscalatorSizeFromDataSource();
+ }
+ }
+
+ /**
+ * Grid does not support adding Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @param w
+ * irrelevant
+ * @throws UnsupportedOperationException
+ * always
+ */
+ @Override
+ @Deprecated
+ public void add(Widget w) {
+ throw new UnsupportedOperationException(
+ "Cannot add widgets to Grid with this method");
+ }
+
+ /**
+ * Grid does not support clearing Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @throws UnsupportedOperationException
+ * always
+ */
+ @Override
+ @Deprecated
+ public void clear() {
+ throw new UnsupportedOperationException(
+ "Cannot clear widgets from Grid this way");
+ }
+
+ /**
+ * Grid does not support iterating through Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @return never
+ * @throws UnsupportedOperationException
+ * always
+ */
+ @Override
+ @Deprecated
+ public Iterator<Widget> iterator() {
+ throw new UnsupportedOperationException(
+ "Cannot iterate through widgets in Grid this way");
+ }
+
+ /**
+ * Grid does not support removing Widgets this way.
+ * <p>
+ * This method is implemented only because removing widgets from Grid (added
+ * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface.
+ *
+ * @return always <code>false</code>
+ */
+ @Override
+ @Deprecated
+ public boolean remove(Widget w) {
+ /*
+ * This is the method that is the sole reason to have Grid implement
+ * HasWidget - when Vaadin removes a Component from the hierarchy, the
+ * corresponding Widget will call removeFromParent() on itself. GWT will
+ * check there that its parent (i.e. Grid) implements HasWidgets, and
+ * will call this remove(Widget) method.
+ *
+ * tl;dr: all this song and dance to make sure GWT's sanity checks
+ * aren't triggered, even though they effectively do nothing interesting
+ * from Grid's perspective.
+ */
+ return false;
+ }
+
+ /**
+ * Accesses the package private method Widget#setParent()
+ *
+ * @param widget
+ * The widget to access
+ * @param parent
+ * The parent to set
+ */
+ private static native final void setParent(Widget widget, Grid<?> parent)
+ /*-{
+ widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent);
+ }-*/;
+
+ /**
+ * Resets all cached pixel sizes and reads new values from the DOM. This
+ * methods should be used e.g. when styles affecting the dimensions of
+ * elements in this grid have been changed.
+ */
+ public void resetSizesFromDom() {
+ getEscalator().resetSizesFromDom();
+ }
+}
diff --git a/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java
new file mode 100644
index 0000000000..24ccd6c57e
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * 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.client.ui.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.widget.grid.datasources.ListDataSource;
+
+public class ListDataSourceTest {
+
+ @Test
+ public void testDataSourceConstruction() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ assertEquals(4, ds.size());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+
+ ds = new ListDataSource<Integer>(Arrays.asList(0, 1, 2, 3));
+
+ assertEquals(4, ds.size());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+ }
+
+ @Test
+ public void testListAddOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataAdded(4, 1);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().add(4);
+
+ assertEquals(5, ds.size());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+ assertEquals(4, (int) ds.getRow(4));
+ }
+
+ @Test
+ public void testListAddAllOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataAdded(4, 3);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().addAll(Arrays.asList(4, 5, 6));
+
+ assertEquals(7, ds.size());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+ assertEquals(4, (int) ds.getRow(4));
+ assertEquals(5, (int) ds.getRow(5));
+ assertEquals(6, (int) ds.getRow(6));
+ }
+
+ @Test
+ public void testListRemoveOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataRemoved(3, 1);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().remove(2);
+
+ assertEquals(3, ds.size());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(3, (int) ds.getRow(2));
+ }
+
+ @Test
+ public void testListRemoveAllOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataRemoved(0, 3);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().removeAll(Arrays.asList(0, 2, 3));
+
+ assertEquals(1, ds.size());
+ assertEquals(1, (int) ds.getRow(0));
+ }
+
+ @Test
+ public void testListClearOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataRemoved(0, 4);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().clear();
+
+ assertEquals(0, ds.size());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testFetchingNonExistantItem() {
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+ ds.ensureAvailability(5, 1);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testUnsupportedIteratorRemove() {
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+ ds.asList().iterator().remove();
+ }
+
+ @Test
+ public void sortColumn() {
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(3, 4, 2, 3, 1);
+
+ // TODO Should be simplified to sort(). No point in providing these
+ // trivial comparators.
+ ds.sort(new Comparator<Integer>() {
+ @Override
+ public int compare(Integer o1, Integer o2) {
+ return o1.compareTo(o2);
+ }
+ });
+
+ assertTrue(Arrays.equals(ds.asList().toArray(), new Integer[] { 1, 2,
+ 3, 3, 4 }));
+ }
+
+}
diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
new file mode 100644
index 0000000000..e97bb339e4
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * 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.client.ui.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.shared.ui.grid.Range;
+
+@SuppressWarnings("static-method")
+public class PartitioningTest {
+
+ @Test
+ public void selfRangeTest() {
+ final Range range = Range.between(0, 10);
+ final Range[] partitioning = range.partitionWith(range);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertTrue("inside is self", partitioning[1].equals(range));
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void beforeRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(10, 20);
+ final Range[] partitioning = beforeRange.partitionWith(afterRange);
+
+ assertTrue("before is self", partitioning[0].equals(beforeRange));
+ assertTrue("inside is empty", partitioning[1].isEmpty());
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void afterRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(10, 20);
+ final Range[] partitioning = afterRange.partitionWith(beforeRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertTrue("inside is empty", partitioning[1].isEmpty());
+ assertTrue("after is self", partitioning[2].equals(afterRange));
+ }
+
+ @Test
+ public void beforeAndInsideRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(5, 15);
+ final Range[] partitioning = beforeRange.partitionWith(afterRange);
+
+ assertEquals("before", Range.between(0, 5), partitioning[0]);
+ assertEquals("inside", Range.between(5, 10), partitioning[1]);
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void insideRangeTest() {
+ final Range fullRange = Range.between(0, 20);
+ final Range insideRange = Range.between(5, 15);
+ final Range[] partitioning = insideRange.partitionWith(fullRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertEquals("inside", Range.between(5, 15), partitioning[1]);
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void insideAndBelowTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(5, 15);
+ final Range[] partitioning = afterRange.partitionWith(beforeRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertEquals("inside", Range.between(5, 10), partitioning[1]);
+ assertEquals("after", Range.between(10, 15), partitioning[2]);
+ }
+
+ @Test
+ public void aboveAndBelowTest() {
+ final Range fullRange = Range.between(0, 20);
+ final Range insideRange = Range.between(5, 15);
+ final Range[] partitioning = fullRange.partitionWith(insideRange);
+
+ assertEquals("before", Range.between(0, 5), partitioning[0]);
+ assertEquals("inside", Range.between(5, 15), partitioning[1]);
+ assertEquals("after", Range.between(15, 20), partitioning[2]);
+ }
+}
diff --git a/common.xml b/common.xml
index 80a1cbf642..8c2919972d 100644
--- a/common.xml
+++ b/common.xml
@@ -9,7 +9,7 @@
<property name="gwt.basedir" location="${vaadin.basedir}/../gwt" />
<property file="${vaadin.basedir}/build.properties" />
- <property name="modules.to.publish.to.maven" value="shared,server,client,client-compiler,client-compiled,themes,push" />
+ <property name="modules.to.publish.to.maven" value="shared,server,client,client-compiler,client-compiled,themes,push,widgets" />
<property name="modules.to.publish.to.download" value="${modules.to.publish.to.maven},all" />
<ivy:settings file="${vaadin.basedir}/ivysettings.xml" />
@@ -30,83 +30,11 @@
<union id="empty.reference" />
- <property name="filtered.webcontent.dir" location="${vaadin.basedir}/result/filteredWebContent" />
- <property name="release-notes-tickets-file" location="${vaadin.basedir}/result/release-notes-tickets.html" />
- <property name="release-notes-authors-file" location="${vaadin.basedir}/result/release-notes-authors.html" />
-
- <target name="filter.webcontent" unless="webcontent.filtered" depends="fetch-release-notes-tickets,fetch-release-notes-authors">
- <property name="webcontent.filtered" value="true" />
- <!-- Running without build.release-notes will cause an error, which
- is ignored -->
- <loadfile property="release-notes-tickets" srcFile="${release-notes-tickets-file}" failonerror="false" />
- <loadfile property="release-notes-authors" srcFile="${release-notes-authors-file}" failonerror="false" />
-
- <delete dir="${filtered.webcontent.dir}" />
- <copy todir="${filtered.webcontent.dir}">
- <fileset dir="${vaadin.basedir}/WebContent">
- <include name="img/**" />
- </fileset>
- </copy>
- <copy todir="${filtered.webcontent.dir}">
- <fileset dir="${vaadin.basedir}/WebContent">
- <patternset>
- <include name="release-notes.html" />
- <include name="license.html" />
- <include name="licenses/**" />
- <include name="css/**" />
- </patternset>
- </fileset>
- <filterchain>
- <expandproperties />
- <replacetokens begintoken="@" endtoken="@">
- <token key="version" value="${vaadin.version}" />
- </replacetokens>
- <replacetokens begintoken="@" endtoken="@">
- <token key="version-minor" value="${vaadin.version.major}.${vaadin.version.minor}" />
- </replacetokens>
- <replacetokens begintoken="@" endtoken="@">
- <token key="builddate" value="${build.date}" />
- </replacetokens>
- <replacetokens begintoken="@" endtoken="@">
- <token key="release-notes-tickets" value="${release-notes-tickets}" />
- </replacetokens>
- <replacetokens begintoken="@" endtoken="@">
- <token key="release-notes-authors" value="${release-notes-authors}" />
- </replacetokens>
- </filterchain>
- </copy>
- </target>
-
- <target name="fetch-release-notes-tickets" unless="built.release-notes-tickets" if="build.release-notes">
- <mkdir dir="${vaadin.basedir}/result" />
- <subant buildpath="${vaadin.basedir}/buildhelpers" target="fetch-release-notes-tickets" antfile="build.xml" inheritall="true">
- <property name="output" location="${release-notes-tickets-file}" />
- </subant>
- <property name="built.release-notes-tickets" value="1" />
- </target>
-
- <target name="fetch-release-notes-authors" unless="built.release-notes-authors" if="build.release-notes">
- <mkdir dir="${vaadin.basedir}/result" />
- <subant buildpath="${vaadin.basedir}/buildhelpers" target="fetch-release-notes-authors" antfile="build.xml" inheritall="true">
- <property name="output" location="${release-notes-authors-file}" />
- </subant>
- <property name="built.release-notes-authors" value="1" />
- </target>
-
- <fileset dir="${filtered.webcontent.dir}" id="common.files.for.all.jars">
- <patternset>
- <include name="release-notes.html" />
- <include name="license.html" />
- <include name="licenses/**" />
- <include name="css/**" />
- <include name="img/**" />
- </patternset>
- </fileset>
-
-
<target name="pom.xml" description="Generates a pom.xml based on the Ivy configuration. Either for a snapshot or a release version" depends="pom.xml.release,pom.xml.snapshot">
</target>
+ <property name="common.jarfiles.dir" location="${vaadin.basedir}/buildhelpers/result/WebContent" />
+
<target name="pom.xml.release" if="build.release">
<fail unless="result.dir" message="No result.dir parameter given" />
<property name="ivy.xml" location="${result.dir}/../ivy.xml" />
@@ -141,7 +69,7 @@
</target>
- <target name="sources.jar" depends="compile, filter.webcontent">
+ <target name="sources.jar" depends="compile">
<fail unless="result.dir" message="No result.dir parameter given" />
<fail unless="module.name" message="No module.name parameter given" />
<fail unless="src" message="No src directory parameter given" />
@@ -155,7 +83,7 @@
<include name="**/*.properties" />
</patternset>
</fileset>
- <fileset refid="common.files.for.all.jars" />
+ <fileset dir="${common.jarfiles.dir}" />
<restrict>
<union refid="extra.jar.includes" />
<name name="*.java" />
@@ -164,8 +92,8 @@
</target>
- <target name="javadoc.jar" depends="dependencies, filter.webcontent">
- <fail unless="result.dir" message="No result.dir parameter given" />
+ <target name="javadoc.jar" depends="dependencies">
+ <fail unless="result.dir" message="No result.dir parameter given" />
<fail unless="module.name" message="No module.name parameter given" />
<property name="src" location="{$result.dir}/../src" />
<property name="javadoc.dir" value="${result.dir}/javadoc" />
@@ -178,34 +106,34 @@
out without using conf attribute. Using conf would make internal
dependency resolution unnecessary complicated.
-->
- <isset property="nojavadoc" />
- <then>
+ <isset property="nojavadoc" />
+ <then>
<jar file="${javadoc.jar}" compress="true">
- <fileset refid="common.files.for.all.jars" />
+ <fileset dir="${common.jarfiles.dir}" />
</jar>
- </then>
+ </then>
<else>
- <javadoc destdir="${javadoc.dir}" author="true" version="true" use="true" windowtitle="${module.name}">
- <packageset dir="${src}" excludes="${classes.exclude}" />
-
- <doctitle>&lt;h1>${module.name}&lt;/h1></doctitle>
- <!-- <header><![CDATA[<script type="text/javascript" src=".html-style/style.js"></script>]]></header> -->
- <bottom>${javadoc.bottom}</bottom>
- <link offline="true" href="http://docs.oracle.com/javase/6/docs/api/" packagelistLoc="build/javadoc/j2se-1.6.0" />
- <link offline="true" href="http://java.sun.com/j2ee/1.4/docs/api/" packagelistLoc="build/javadoc/j2ee-1.4" />
- <classpath refid="classpath.compile.dependencies" />
- </javadoc>
-
- <!-- Create a javadoc jar -->
- <jar file="${javadoc.jar}" compress="true">
- <fileset dir="${javadoc.dir}" />
- <fileset refid="common.files.for.all.jars" />
- </jar>
+ <javadoc destdir="${javadoc.dir}" author="true" version="true" use="true" windowtitle="${module.name}">
+ <packageset dir="${src}" excludes="${classes.exclude}" />
+
+ <doctitle>&lt;h1>${module.name}&lt;/h1></doctitle>
+ <!-- <header><![CDATA[<script type="text/javascript" src=".html-style/style.js"></script>]]></header> -->
+ <bottom>${javadoc.bottom}</bottom>
+ <link offline="true" href="http://docs.oracle.com/javase/6/docs/api/" packagelistLoc="build/javadoc/j2se-1.6.0" />
+ <link offline="true" href="http://java.sun.com/j2ee/1.4/docs/api/" packagelistLoc="build/javadoc/j2ee-1.4" />
+ <classpath refid="classpath.compile.dependencies" />
+ </javadoc>
+
+ <!-- Create a javadoc jar -->
+ <jar file="${javadoc.jar}" compress="true">
+ <fileset dir="${javadoc.dir}" />
+ <fileset dir="${common.jarfiles.dir}" />
+ </jar>
</else>
</antcontrib:if>
</target>
- <target name="jar" depends="compile, pom.xml, filter.webcontent">
+ <target name="jar" depends="compile, pom.xml">
<fail unless="result.dir" message="No result.dir parameter given" />
<fail unless="module.name" message="No module.name parameter given" />
@@ -214,9 +142,9 @@
<property name="src" location="{$result.dir}/../src" />
<union id="jar.files">
- <fileset dir="${classes}" excludes="${classes.exclude}" erroronmissingdir="false"/>
+ <fileset dir="${classes}" excludes="${classes.exclude}" erroronmissingdir="false" />
<fileset dir="${src}" excludes="${jar.exclude}" erroronmissingdir="false" />
- <fileset refid="common.files.for.all.jars" />
+ <fileset dir="${common.jarfiles.dir}" />
<union refid="extra.jar.includes" />
</union>
@@ -291,11 +219,14 @@
</then>
</antcontrib:if>
+ <ivy:resolve inline="true" organisation="com.vaadin" module="vaadin-buildhelpers" revision="${vaadin.version}" keep="true" />
+ <ivy:cachepath pathid="buildhelpers.classpath" />
+
<!-- Generate the Export-Package attribute in the manifest -->
<java classname="com.vaadin.buildhelpers.GeneratePackageExports" failonerror="true" fork="yes">
<arg value="${jar}" />
<arg line="com/vaadin com/google ${osgi.extra.package.prefixes}" />
- <classpath refid="vaadin.buildhelpers.classpath" />
+ <classpath refid="buildhelpers.classpath" />
<jvmarg value="-Dvaadin.version=${vaadin.version}" />
</java>
</target>
@@ -308,20 +239,10 @@
<classpath refid="classpath.compile.custom" />
</javac>
<copy todir="${classes}">
- <fileset dir="${src}" includes="${extra.classes}"/>
+ <fileset dir="${src}" includes="${extra.classes}" />
</copy>
</target>
- <target name="exec-buildhelper" depends="compile">
- <fail unless="main.class" message="No main class given in 'main.class'" />
- <fail unless="output" message="No output file given in 'output'" />
- <java classname="${main.class}" output="${output}" failonerror="true" fork="yes">
- <classpath refid="vaadin.buildhelpers.classpath" />
- <classpath refid="classpath.compile.dependencies" />
- <jvmarg value="-Dvaadin.version=${vaadin.version}" />
- </java>
- </target>
-
<target name="directories">
<property name="result.dir" location="result" />
<property name="src" location="${result.dir}/../src" />
@@ -380,10 +301,10 @@
<!-- Copy resources -->
<copy todir="${test.classes}" failonerror="false">
<fileset dir="${test.resources}" />
- <!-- include html templates used in declarative tests -->
- <fileset dir="${test.src}">
- <include name="**/*.html"/>
- </fileset>
+ <!-- include html templates used in declarative tests -->
+ <fileset dir="${test.src}">
+ <include name="**/*.html" />
+ </fileset>
</copy>
</target>
diff --git a/ivysettings.xml b/ivysettings.xml
index 981ef2006d..bc60be5e29 100644
--- a/ivysettings.xml
+++ b/ivysettings.xml
@@ -43,6 +43,8 @@
resolver="build-temp" />
<module organisation="com.vaadin" name="vaadin-push"
resolver="build-temp" />
+ <module organisation="com.vaadin" name="vaadin-widgets"
+ resolver="build-temp" />
<module organisation="com.vaadin" name="vaadin-liferay"
resolver="build-temp" />
</modules>
diff --git a/push/build.xml b/push/build.xml
index b7d57cf4d3..9afe76620c 100644
--- a/push/build.xml
+++ b/push/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-push" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-push" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
Meta package which defines dependencies needed for push
</description>
@@ -13,7 +14,8 @@
<property name="temp.dir" location="${result.dir}/temp" />
<property name="jquery.unpack" location="${temp.dir}/jquery" />
<property name="vaadinPush.js" location="${result.dir}/js/VAADIN/vaadinPush.js" />
- <property name="vaadinPush.debug.js" location="${result.dir}/js/VAADIN/vaadinPush.debug.js" />
+ <property name="vaadinPush.debug.js"
+ location="${result.dir}/js/VAADIN/vaadinPush.debug.js" />
<!-- Keep the version number in sync with ivy.xml, server/src/com/vaadin/server/Constants.java -->
<property name="atmosphere.runtime.version" value="2.2.4.vaadin2" />
@@ -30,8 +32,10 @@
<target name="vaadinPush.js">
<mkdir dir="${result.dir}/js/VAADIN" />
- <ivy:resolve log="download-only" file="ivy.xml" conf="push.js" />
- <ivy:cachepath pathid="atmosphere.jquery.deps" conf="push.js" />
+ <ivy:resolve log="download-only" file="ivy.xml"
+ conf="push.js" />
+ <ivy:cachepath pathid="atmosphere.jquery.deps"
+ conf="push.js" />
<delete dir="${temp.dir}" />
<copy flatten="true" tofile="${temp.dir}/jquery.war">
@@ -46,9 +50,12 @@
<mapper type="flatten" />
</unzip>
<loadfile srcfile="${jquery.js}" property="jquery.js.contents" />
- <loadfile srcfile="${jquery.unpack}/jquery.atmosphere.js" property="jquery.atmosphere.js.contents" />
+ <loadfile srcfile="${jquery.unpack}/jquery.atmosphere.js"
+ property="jquery.atmosphere.js.contents" />
- <loadfile srcfile="${vaadin.basedir}/WebContent/VAADIN/vaadinPush.js.tpl" property="vaadinPush.js.contents">
+ <loadfile
+ srcfile="${vaadin.basedir}/WebContent/VAADIN/vaadinPush.js.tpl"
+ property="vaadinPush.js.contents">
<filterchain>
<replacetokens begintoken="@" endtoken="@">
<token key="jquery.js" value="${jquery.js.contents}" />
@@ -61,7 +68,9 @@
<echo file="${vaadinPush.debug.js}">${vaadinPush.js.contents}</echo>
<!-- Minify -->
- <ivy:retrieve organisation="com.yahoo.platform.yui" module="yuicompressor" revision="2.4.7" inline="true" type="jar" pattern="${result.dir}/compressor.jar" />
+ <ivy:retrieve organisation="com.yahoo.platform.yui"
+ module="yuicompressor" revision="2.4.7" inline="true" type="jar"
+ pattern="${result.dir}/compressor.jar" />
<java jar="${result.dir}/compressor.jar" fork="true">
<arg value="-v" />
<arg value="-o" />
@@ -72,7 +81,8 @@
<target name="jar" depends="vaadinPush.js">
<antcall target="common.jar">
- <param name="require-bundle" value="com.vaadin.external.atmosphere.runtime;bundle-version=&quot;${atmosphere.runtime.version}&quot;;visibility:=reexport" />
+ <param name="require-bundle"
+ value="com.vaadin.external.atmosphere.runtime;bundle-version=&quot;${atmosphere.runtime.version}&quot;;visibility:=reexport" />
<reference torefid="extra.jar.includes" refid="jar.includes" />
</antcall>
</target>
diff --git a/server/build.xml b/server/build.xml
index 7bb70ffdc4..c658ab0336 100644
--- a/server/build.xml
+++ b/server/build.xml
@@ -1,8 +1,10 @@
<?xml version="1.0"?>
-<project name="vaadin-server" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-server" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
- Compiles build helpers used when building other modules.
+ Compiles build helpers used when building other
+ modules.
</description>
<include file="../common.xml" as="common" />
<include file="../build.xml" as="vaadin" />
@@ -23,8 +25,10 @@
</union>
<target name="jar">
- <property name="server.osgi.import" value="javax.servlet;version=&quot;2.4.0&quot;,javax.servlet.http;version=&quot;2.4.0&quot;,javax.validation;version=&quot;1.0.0.GA&quot;;resolution:=optional,org.jsoup;version=&quot;1.6.3&quot;,org.jsoup.parser;version=&quot;1.6.3&quot;,org.jsoup.nodes;version=&quot;1.6.3&quot;,org.jsoup.helper;version=&quot;1.6.3&quot;,org.jsoup.safety;version=&quot;1.6.3&quot;,org.json;version=&quot;0.0.20080701&quot;" />
- <property name="server.osgi.require" value="com.vaadin.shared;bundle-version=&quot;${vaadin.version}&quot;,com.vaadin.push;bundle-version=&quot;${vaadin.version}&quot;;resolution:=optional,com.vaadin.sass-compiler;bundle-version=&quot;${vaadin.sass.version}&quot;;resolution:=optional" />
+ <property name="server.osgi.import"
+ value="javax.servlet;version=&quot;2.4.0&quot;,javax.servlet.http;version=&quot;2.4.0&quot;,javax.validation;version=&quot;1.0.0.GA&quot;;resolution:=optional,org.jsoup;version=&quot;1.6.3&quot;,org.jsoup.parser;version=&quot;1.6.3&quot;,org.jsoup.nodes;version=&quot;1.6.3&quot;,org.jsoup.helper;version=&quot;1.6.3&quot;,org.jsoup.safety;version=&quot;1.6.3&quot;" />
+ <property name="server.osgi.require"
+ value="com.vaadin.shared;bundle-version=&quot;${vaadin.version}&quot;,com.vaadin.push;bundle-version=&quot;${vaadin.version}&quot;;resolution:=optional,com.vaadin.sass-compiler;bundle-version=&quot;${vaadin.sass.version}&quot;;resolution:=optional" />
<antcall target="common.jar">
<param name="require-bundle" value="${server.osgi.require}" />
<param name="import-package" value="${server.osgi.import}" />
diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java
index 8e99bac541..fb7a93e832 100644
--- a/server/src/com/vaadin/data/Container.java
+++ b/server/src/com/vaadin/data/Container.java
@@ -582,6 +582,64 @@ public interface Container extends Serializable {
public Item addItemAt(int index, Object newItemId)
throws UnsupportedOperationException;
+ /**
+ * An <code>Event</code> object specifying information about the added
+ * items.
+ *
+ * @since 7.4
+ */
+ public interface ItemAddEvent extends ItemSetChangeEvent {
+
+ /**
+ * Gets the item id of the first added item.
+ *
+ * @return item id of the first added item
+ */
+ public Object getFirstItemId();
+
+ /**
+ * Gets the index of the first added item.
+ *
+ * @return index of the first added item
+ */
+ public int getFirstIndex();
+
+ /**
+ * Gets the number of the added items.
+ *
+ * @return the number of added items.
+ */
+ public int getAddedItemsCount();
+ }
+
+ /**
+ * An <code>Event</code> object specifying information about the removed
+ * items.
+ *
+ * @since 7.4
+ */
+ public interface ItemRemoveEvent extends ItemSetChangeEvent {
+ /**
+ * Gets the item id of the first removed item.
+ *
+ * @return item id of the first removed item
+ */
+ public Object getFirstItemId();
+
+ /**
+ * Gets the index of the first removed item.
+ *
+ * @return index of the first removed item
+ */
+ public int getFirstIndex();
+
+ /**
+ * Gets the number of the removed items.
+ *
+ * @return the number of removed items
+ */
+ public int getRemovedItemsCount();
+ }
}
/**
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
new file mode 100644
index 0000000000..5da95c3b5c
--- /dev/null
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -0,0 +1,1045 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.thirdparty.guava.common.collect.BiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.Converter.ConversionException;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.CellReference;
+import com.vaadin.ui.Grid.CellStyleGenerator;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.RowReference;
+import com.vaadin.ui.Grid.RowStyleGenerator;
+import com.vaadin.ui.renderer.Renderer;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * Provides Vaadin server-side container data source to a
+ * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
+ * implemented as an Extension hardcoded to support a specific connector type.
+ * This will be changed once framework support for something more flexible has
+ * been implemented.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RpcDataProviderExtension extends AbstractExtension {
+
+ /**
+ * ItemId to Key to ItemId mapper.
+ * <p>
+ * This class is used when transmitting information about items in container
+ * related to Grid. It introduces a consistent way of mapping ItemIds and
+ * its container to a String that can be mapped back to ItemId.
+ * <p>
+ * <em>Technical note:</em> This class also keeps tabs on which indices are
+ * being shown/selected, and is able to clean up after itself once the
+ * itemId &lrarr; key mapping is not needed anymore. In other words, this
+ * doesn't leak memory.
+ */
+ public class DataProviderKeyMapper implements Serializable {
+ private final BiMap<Integer, Object> indexToItemId = HashBiMap.create();
+ private final BiMap<Object, String> itemIdToKey = HashBiMap.create();
+ private Set<Object> pinnedItemIds = new HashSet<Object>();
+ private Range activeRange = Range.withLength(0, 0);
+ private long rollingIndex = 0;
+
+ private DataProviderKeyMapper() {
+ // private implementation
+ }
+
+ void setActiveRange(Range newActiveRange) {
+ final Range[] removed = activeRange.partitionWith(newActiveRange);
+ final Range[] added = newActiveRange.partitionWith(activeRange);
+
+ removeActiveRows(removed[0]);
+ removeActiveRows(removed[2]);
+ addActiveRows(added[0]);
+ addActiveRows(added[2]);
+
+ activeRange = newActiveRange;
+ }
+
+ private void removeActiveRows(final Range deprecated) {
+ for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
+ final Integer ii = Integer.valueOf(i);
+ final Object itemId = indexToItemId.get(ii);
+
+ if (!isPinned(itemId)) {
+ itemIdToKey.remove(itemId);
+ indexToItemId.remove(ii);
+ }
+ }
+ }
+
+ private void addActiveRows(Range added) {
+ if (added.isEmpty()) {
+ // Some container.getItemIds() implementations just might be
+ // expensive even for an empty range, so bail out early
+ return;
+ }
+
+ List<?> newItemIds = container.getItemIds(added.getStart(),
+ added.length());
+ Integer index = added.getStart();
+ for (Object itemId : newItemIds) {
+ /*
+ * We might be in a situation we have an index <-> itemId entry
+ * already. This happens when something was selected, scrolled
+ * out of view and now we're scrolling it back into view. It's
+ * unnecessary to overwrite it in that case.
+ *
+ * Fun thought: considering branch prediction, it _might_ even
+ * be a bit faster to simply always run the code inside this
+ * if-state. But it sounds too stupid (and most often too
+ * insignificant) to try out.
+ */
+ if (!indexToItemId.containsKey(index)) {
+ /*
+ * We might be in a situation where we have an itemId <->
+ * key entry already, but no index for it. This happens when
+ * something that is out of view is selected
+ * programmatically. In that case, we only want to add an
+ * index for that entry, and not overwrite the key.
+ */
+ if (!itemIdToKey.containsKey(itemId)) {
+ itemIdToKey.put(itemId, nextKey());
+ }
+
+ indexToItemId.forcePut(index, itemId);
+ }
+ index++;
+ }
+ }
+
+ private String nextKey() {
+ return String.valueOf(rollingIndex++);
+ }
+
+ String getKey(Object itemId) {
+ String key = itemIdToKey.get(itemId);
+ if (key == null) {
+ key = nextKey();
+ itemIdToKey.put(itemId, key);
+ }
+ return key;
+ }
+
+ /**
+ * Gets keys for a collection of item ids.
+ * <p>
+ * If the itemIds are currently cached, the existing keys will be used.
+ * Otherwise new ones will be created.
+ *
+ * @param itemIds
+ * the item ids for which to get keys
+ * @return keys for the {@code itemIds}
+ */
+ public List<String> getKeys(Collection<Object> itemIds) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds can't be null");
+ }
+
+ ArrayList<String> keys = new ArrayList<String>(itemIds.size());
+ for (Object itemId : itemIds) {
+ keys.add(getKey(itemId));
+ }
+ return keys;
+ }
+
+ /**
+ * Gets the registered item id based on its key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a
+ * client. This method can be used to get the item id for the row key
+ * that the client has sent.
+ *
+ * @param key
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ * @throws IllegalStateException
+ * if the key mapper does not have a record of {@code key} .
+ */
+ public Object getItemId(String key) throws IllegalStateException {
+ Object itemId = itemIdToKey.inverse().get(key);
+ if (itemId != null) {
+ return itemId;
+ } else {
+ throw new IllegalStateException("No item id for key " + key
+ + " found.");
+ }
+ }
+
+ /**
+ * Gets corresponding item ids for each of the keys in a collection.
+ *
+ * @param keys
+ * the keys for which to retrieve item ids
+ * @return a collection of item ids for the {@code keys}
+ * @throws IllegalStateException
+ * if one or more of keys don't have a corresponding item id
+ * in the cache
+ */
+ public Collection<Object> getItemIds(Collection<String> keys)
+ throws IllegalStateException {
+ if (keys == null) {
+ throw new IllegalArgumentException("keys may not be null");
+ }
+
+ ArrayList<Object> itemIds = new ArrayList<Object>(keys.size());
+ for (String key : keys) {
+ itemIds.add(getItemId(key));
+ }
+ return itemIds;
+ }
+
+ /**
+ * Pin an item id to be cached indefinitely.
+ * <p>
+ * Normally when an itemId is not an active row, it is discarded from
+ * the cache. Pinning an item id will make sure that it is kept in the
+ * cache.
+ * <p>
+ * In effect, while an item id is pinned, it always has the same key.
+ *
+ * @param itemId
+ * the item id to pin
+ * @throws IllegalStateException
+ * if {@code itemId} was already pinned
+ * @see #unpin(Object)
+ * @see #isPinned(Object)
+ * @see #getItemIds(Collection)
+ */
+ public void pin(Object itemId) throws IllegalStateException {
+ if (isPinned(itemId)) {
+ throw new IllegalStateException("Item id " + itemId
+ + " was pinned already");
+ }
+ pinnedItemIds.add(itemId);
+ }
+
+ /**
+ * Unpin an item id.
+ * <p>
+ * This cancels the effect of pinning an item id. If the item id is
+ * currently inactive, it will be immediately removed from the cache.
+ *
+ * @param itemId
+ * the item id to unpin
+ * @throws IllegalStateException
+ * if {@code itemId} was not pinned
+ * @see #pin(Object)
+ * @see #isPinned(Object)
+ * @see #getItemIds(Collection)
+ */
+ public void unpin(Object itemId) throws IllegalStateException {
+ if (!isPinned(itemId)) {
+ throw new IllegalStateException("Item id " + itemId
+ + " was not pinned");
+ }
+
+ pinnedItemIds.remove(itemId);
+ final Integer index = indexToItemId.inverse().get(itemId);
+ if (index == null || !activeRange.contains(index.intValue())) {
+ itemIdToKey.remove(itemId);
+ indexToItemId.remove(index);
+ }
+ }
+
+ /**
+ * Checks whether an item id is pinned or not.
+ *
+ * @param itemId
+ * the item id to check for pin status
+ * @return {@code true} iff the item id is currently pinned
+ */
+ public boolean isPinned(Object itemId) {
+ return pinnedItemIds.contains(itemId);
+ }
+
+ Object itemIdAtIndex(int index) {
+ return indexToItemId.get(Integer.valueOf(index));
+ }
+ }
+
+ /**
+ * A helper class that handles the client-side Escalator logic relating to
+ * making sure that whatever is currently visible to the user, is properly
+ * initialized and otherwise handled on the server side (as far as
+ * required).
+ * <p>
+ * This bookeeping includes, but is not limited to:
+ * <ul>
+ * <li>listening to the currently visible {@link com.vaadin.data.Property
+ * Properties'} value changes on the server side and sending those back to
+ * the client; and
+ * <li>attaching and detaching {@link com.vaadin.ui.Component Components}
+ * from the Vaadin Component hierarchy.
+ * </ul>
+ */
+ private class ActiveRowHandler implements Serializable {
+ /**
+ * A map from itemId to the value change listener used for all of its
+ * properties
+ */
+ private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>();
+
+ /**
+ * The currently active range. Practically, it's the range of row
+ * indices being cached currently.
+ */
+ private Range activeRange = Range.withLength(0, 0);
+
+ /**
+ * A hook for making sure that appropriate data is "active". All other
+ * rows should be "inactive".
+ * <p>
+ * "Active" can mean different things in different contexts. For
+ * example, only the Properties in the active range need
+ * ValueChangeListeners. Also, whenever a row with a Component becomes
+ * active, it needs to be attached (and conversely, when inactive, it
+ * needs to be detached).
+ *
+ * @param firstActiveRow
+ * the first active row
+ * @param activeRowCount
+ * the number of active rows
+ */
+ public void setActiveRows(int firstActiveRow, int activeRowCount) {
+
+ final Range newActiveRange = Range.withLength(firstActiveRow,
+ activeRowCount);
+
+ // TODO [[Components]] attach and detach components
+
+ /*-
+ * Example
+ *
+ * New Range: [3, 4, 5, 6, 7]
+ * Old Range: [1, 2, 3, 4, 5]
+ * Result: [1, 2][3, 4, 5] []
+ */
+ final Range[] depractionPartition = activeRange
+ .partitionWith(newActiveRange);
+ removeValueChangeListeners(depractionPartition[0]);
+ removeValueChangeListeners(depractionPartition[2]);
+
+ /*-
+ * Example
+ *
+ * Old Range: [1, 2, 3, 4, 5]
+ * New Range: [3, 4, 5, 6, 7]
+ * Result: [] [3, 4, 5][6, 7]
+ */
+ final Range[] activationPartition = newActiveRange
+ .partitionWith(activeRange);
+ addValueChangeListeners(activationPartition[0]);
+ addValueChangeListeners(activationPartition[2]);
+
+ activeRange = newActiveRange;
+
+ assert valueChangeListeners.size() == newActiveRange.length() : "Value change listeners not set up correctly!";
+ }
+
+ private void addValueChangeListeners(Range range) {
+ for (int i = range.getStart(); i < range.getEnd(); i++) {
+
+ final Object itemId = container.getIdByIndex(i);
+ final Item item = container.getItem(itemId);
+
+ if (valueChangeListeners.containsKey(itemId)) {
+ /*
+ * This might occur when items are removed from above the
+ * viewport, the escalator scrolls up to compensate, but the
+ * same items remain in the view: It looks as if one row was
+ * scrolled, when in fact the whole viewport was shifted up.
+ */
+ continue;
+ }
+
+ GridValueChangeListener listener = new GridValueChangeListener(
+ itemId, item);
+ valueChangeListeners.put(itemId, listener);
+ }
+ }
+
+ private void removeValueChangeListeners(Range range) {
+ for (int i = range.getStart(); i < range.getEnd(); i++) {
+ final Object itemId = container.getIdByIndex(i);
+ final GridValueChangeListener listener = valueChangeListeners
+ .remove(itemId);
+
+ if (listener != null) {
+ listener.removeListener();
+ }
+ }
+ }
+
+ /**
+ * Manages removed columns in active rows.
+ * <p>
+ * This method does <em>not</em> send data again to the client.
+ *
+ * @param removedColumns
+ * the columns that have been removed from the grid
+ */
+ public void columnsRemoved(Collection<Column> removedColumns) {
+ if (removedColumns.isEmpty()) {
+ return;
+ }
+
+ for (GridValueChangeListener listener : valueChangeListeners
+ .values()) {
+ listener.removeColumns(removedColumns);
+ }
+ }
+
+ /**
+ * Manages added columns in active rows.
+ * <p>
+ * This method sends the data for the changed rows to client side.
+ *
+ * @param addedColumns
+ * the columns that have been added to the grid
+ */
+ public void columnsAdded(Collection<Column> addedColumns) {
+ if (addedColumns.isEmpty()) {
+ return;
+ }
+
+ for (GridValueChangeListener listener : valueChangeListeners
+ .values()) {
+ listener.addColumns(addedColumns);
+ }
+ }
+
+ /**
+ * Handles the insertion of rows.
+ * <p>
+ * This method's responsibilities are to:
+ * <ul>
+ * <li>shift the internal bookkeeping by <code>count</code> if the
+ * insertion happens above currently active range
+ * <li>ignore rows inserted below the currently active range
+ * <li>shift (and deactivate) rows pushed out of view
+ * <li>activate rows that are inserted in the current viewport
+ * </ul>
+ *
+ * @param firstIndex
+ * the index of the first inserted rows
+ * @param count
+ * the number of rows inserted at <code>firstIndex</code>
+ */
+ public void insertRows(int firstIndex, int count) {
+ if (firstIndex < activeRange.getStart()) {
+ activeRange = activeRange.offsetBy(count);
+ } else if (firstIndex < activeRange.getEnd()) {
+ final Range deprecatedRange = Range.withLength(
+ activeRange.getEnd(), count);
+ removeValueChangeListeners(deprecatedRange);
+
+ final Range freshRange = Range.withLength(firstIndex, count);
+ addValueChangeListeners(freshRange);
+ } else {
+ // out of view, noop
+ }
+ }
+
+ /**
+ * Handles the removal of rows.
+ * <p>
+ * This method's responsibilities are to:
+ * <ul>
+ * <li>shift the internal bookkeeping by <code>count</code> if the
+ * removal happens above currently active range
+ * <li>ignore rows removed below the currently active range
+ * </ul>
+ *
+ * @param firstIndex
+ * the index of the first removed rows
+ * @param count
+ * the number of rows removed at <code>firstIndex</code>
+ */
+ public void removeRows(int firstIndex, int count) {
+ int lastRemoved = firstIndex + count;
+ if (lastRemoved < activeRange.getStart()) {
+ /* firstIndex < lastIndex < start */
+ activeRange = activeRange.offsetBy(-count);
+ } else if (firstIndex < activeRange.getEnd()) {
+ final Range deprecated = Range.between(
+ Math.max(activeRange.getStart(), firstIndex),
+ Math.min(activeRange.getEnd(), lastRemoved + 1));
+ for (int i = deprecated.getStart(); i < deprecated.getEnd(); ++i) {
+ Object itemId = keyMapper.itemIdAtIndex(i);
+ // Item doesn't exist anymore.
+ valueChangeListeners.remove(itemId);
+ }
+
+ activeRange = Range.withLength(activeRange.getStart(),
+ activeRange.length() - deprecated.length());
+ } else {
+ /* end <= firstIndex, no need to do anything */
+ }
+ }
+ }
+
+ /**
+ * A class to listen to changes in property values in the Container added
+ * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies
+ * the data source to update the client-side representation of the modified
+ * item.
+ * <p>
+ * One instance of this class can (and should) be reused for all the
+ * properties in an item, since this class will inform that the entire row
+ * needs to be re-evaluated (in contrast to a property-based change
+ * management)
+ * <p>
+ * Since there's no Container-wide possibility to listen to any kind of
+ * value changes, an instance of this class needs to be attached to each and
+ * every Item's Property in the container.
+ *
+ * @see Grid#addValueChangeListener(Container, Object, Object)
+ * @see Grid#valueChangeListeners
+ */
+ private class GridValueChangeListener implements ValueChangeListener {
+ private final Object itemId;
+ private final Item item;
+
+ public GridValueChangeListener(Object itemId, Item item) {
+ /*
+ * Using an assert instead of an exception throw, just to optimize
+ * prematurely
+ */
+ assert itemId != null : "null itemId not accepted";
+ this.itemId = itemId;
+ this.item = item;
+
+ internalAddColumns(getGrid().getColumns());
+ }
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ updateRowData(itemId);
+ }
+
+ public void removeListener() {
+ removeColumns(getGrid().getColumns());
+ }
+
+ public void addColumns(Collection<Column> addedColumns) {
+ internalAddColumns(addedColumns);
+ updateRowData(itemId);
+ }
+
+ private void internalAddColumns(Collection<Column> addedColumns) {
+ for (final Column column : addedColumns) {
+ final Property<?> property = item.getItemProperty(column
+ .getPropertyId());
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .addValueChangeListener(this);
+ }
+ }
+ }
+
+ public void removeColumns(Collection<Column> removedColumns) {
+ for (final Column column : removedColumns) {
+ final Property<?> property = item.getItemProperty(column
+ .getPropertyId());
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .removeValueChangeListener(this);
+ }
+ }
+ }
+ }
+
+ private final Indexed container;
+
+ private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
+
+ private DataProviderRpc rpc;
+
+ private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+
+ if (event instanceof ItemAddEvent) {
+ ItemAddEvent addEvent = (ItemAddEvent) event;
+ int firstIndex = addEvent.getFirstIndex();
+ int count = addEvent.getAddedItemsCount();
+ insertRowData(firstIndex, count);
+ }
+
+ else if (event instanceof ItemRemoveEvent) {
+ ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
+ int firstIndex = removeEvent.getFirstIndex();
+ int count = removeEvent.getRemovedItemsCount();
+ removeRowData(firstIndex, count);
+ }
+
+ else {
+
+ /*
+ * Clear everything we have in view, and let the client
+ * re-request for whatever it needs.
+ *
+ * Why this shortcut? Well, since anything could've happened, we
+ * don't know what has happened. There are a lot of use-cases we
+ * can cover at once with this carte blanche operation:
+ *
+ * 1) Grid is scrolled somewhere in the middle and all the
+ * rows-inview are removed. We need a new pageful.
+ *
+ * 2) Grid is scrolled somewhere in the middle and none of the
+ * visible rows are removed. We need no new rows.
+ *
+ * 3) Grid is scrolled all the way to the bottom, and the last
+ * rows are being removed. Grid needs to scroll up and request
+ * for more rows at the top.
+ *
+ * 4) Grid is scrolled pretty much to the bottom, and the last
+ * rows are being removed. Grid needs to be aware that some
+ * scrolling is needed, but not to compensate for all the
+ * removed rows. And it also needs to request for some more rows
+ * to the top.
+ *
+ * 5) Some ranges of rows are removed from view. We need to
+ * collapse the gaps with existing rows and load the missing
+ * rows.
+ *
+ * 6) The ultimate use case! Grid has 1.5 pages of rows and
+ * scrolled a bit down. One page of rows is removed. We need to
+ * make sure that new rows are loaded, but not all old slots are
+ * occupied, since the page can't be filled with new row data.
+ * It also needs to be scrolled to the top.
+ *
+ * So, it's easier (and safer) to do the simple thing instead of
+ * taking all the corner cases into account.
+ */
+
+ Map<Object, GridValueChangeListener> listeners = activeRowHandler.valueChangeListeners;
+ for (GridValueChangeListener listener : listeners.values()) {
+ listener.removeListener();
+ }
+ listeners.clear();
+ activeRowHandler.activeRange = Range.withLength(0, 0);
+ keyMapper.setActiveRange(Range.withLength(0, 0));
+ keyMapper.indexToItemId.clear();
+ rpc.resetDataAndSize(event.getContainer().size());
+ }
+ }
+ };
+
+ private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper();
+
+ private KeyMapper<Object> columnKeys;
+
+ /* Has client been initialized */
+ private boolean clientInitialized = false;
+
+ private RowReference rowReference;
+ private CellReference cellReference;
+
+ private Set<Object> updatedItemIds = new HashSet<Object>();
+
+ /**
+ * Creates a new data provider using the given container.
+ *
+ * @param container
+ * the container to make available
+ */
+ public RpcDataProviderExtension(Indexed container) {
+ this.container = container;
+ rpc = getRpcProxy(DataProviderRpc.class);
+
+ registerRpc(new DataRequestRpc() {
+ @Override
+ public void requestRows(int firstRow, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize) {
+
+ pushRowData(firstRow, numberOfRows, firstCachedRowIndex,
+ cacheSize);
+ }
+
+ @Override
+ public void setPinned(String key, boolean isPinned) {
+ Object itemId = keyMapper.getItemId(key);
+ if (isPinned) {
+ // Row might already be pinned if it was selected from the
+ // server
+ if (!keyMapper.isPinned(itemId)) {
+ keyMapper.pin(itemId);
+ }
+ } else {
+ keyMapper.unpin(itemId);
+ }
+ }
+ });
+
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .addItemSetChangeListener(itemListener);
+ }
+
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ if (initial) {
+ clientInitialized = true;
+
+ /*
+ * Push initial set of rows, assuming Grid will initially be
+ * rendered scrolled to the top and with a decent amount of rows
+ * visible. If this guess is right, initial data can be shown
+ * without a round-trip and if it's wrong, the data will simply be
+ * discarded.
+ */
+ int size = container.size();
+ rpc.resetDataAndSize(size);
+
+ int numberOfRows = Math.min(40, size);
+ pushRowData(0, numberOfRows, 0, 0);
+ }
+
+ for (Object itemId : updatedItemIds) {
+ internalUpdateRowData(itemId);
+ }
+ updatedItemIds.clear();
+
+ super.beforeClientResponse(initial);
+ }
+
+ private void pushRowData(int firstRowToPush, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize) {
+ Range active = Range.withLength(firstRowToPush, numberOfRows);
+ if (cacheSize != 0) {
+ Range cached = Range.withLength(firstCachedRowIndex, cacheSize);
+ active = active.combineWith(cached);
+ }
+
+ keyMapper.setActiveRange(active);
+
+ List<?> itemIds = container.getItemIds(firstRowToPush, numberOfRows);
+ JsonArray rows = Json.createArray();
+ for (int i = 0; i < itemIds.size(); ++i) {
+ rows.set(i, getRowData(getGrid().getColumns(), itemIds.get(i)));
+ }
+ rpc.setRowData(firstRowToPush, rows);
+
+ activeRowHandler.setActiveRows(active.getStart(), active.length());
+ }
+
+ private JsonValue getRowData(Collection<Column> columns, Object itemId) {
+ Item item = container.getItem(itemId);
+
+ JsonObject rowData = Json.createObject();
+
+ Grid grid = getGrid();
+
+ for (Column column : columns) {
+ Object propertyId = column.getPropertyId();
+
+ Object propertyValue = item.getItemProperty(propertyId).getValue();
+ JsonValue encodedValue = encodeValue(propertyValue,
+ column.getRenderer(), column.getConverter(),
+ grid.getLocale());
+
+ rowData.put(columnKeys.key(propertyId), encodedValue);
+ }
+
+ final JsonObject rowObject = Json.createObject();
+ rowObject.put(GridState.JSONKEY_DATA, rowData);
+ rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));
+
+ rowReference.set(itemId);
+
+ CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator();
+ if (cellStyleGenerator != null) {
+ setGeneratedCellStyles(cellStyleGenerator, rowObject, columns);
+ }
+ RowStyleGenerator rowStyleGenerator = grid.getRowStyleGenerator();
+ if (rowStyleGenerator != null) {
+ setGeneratedRowStyles(rowStyleGenerator, rowObject);
+ }
+
+ return rowObject;
+ }
+
+ private void setGeneratedCellStyles(CellStyleGenerator generator,
+ JsonObject rowObject, Collection<Column> columns) {
+ JsonObject cellStyles = null;
+ for (Column column : columns) {
+ Object propertyId = column.getPropertyId();
+ cellReference.set(propertyId);
+ String style = generator.getStyle(cellReference);
+ if (style != null) {
+ if (cellStyles == null) {
+ cellStyles = Json.createObject();
+ }
+
+ String columnKey = columnKeys.key(propertyId);
+ cellStyles.put(columnKey, style);
+ }
+ }
+ if (cellStyles != null) {
+ rowObject.put(GridState.JSONKEY_CELLSTYLES, cellStyles);
+ }
+
+ }
+
+ private void setGeneratedRowStyles(RowStyleGenerator generator,
+ JsonObject rowObject) {
+ String rowStyle = generator.getStyle(rowReference);
+ if (rowStyle != null) {
+ rowObject.put(GridState.JSONKEY_ROWSTYLE, rowStyle);
+ }
+ }
+
+ /**
+ * Makes the data source available to the given {@link Grid} component.
+ *
+ * @param component
+ * the remote data grid component to extend
+ */
+ public void extend(Grid component, KeyMapper<Object> columnKeys) {
+ this.columnKeys = columnKeys;
+ super.extend(component);
+ }
+
+ /**
+ * Informs the client side that new rows have been inserted into the data
+ * source.
+ *
+ * @param index
+ * the index at which new rows have been inserted
+ * @param count
+ * the number of rows inserted at <code>index</code>
+ */
+ private void insertRowData(int index, int count) {
+ if (clientInitialized) {
+ rpc.insertRowData(index, count);
+ }
+
+ activeRowHandler.insertRows(index, count);
+ }
+
+ /**
+ * Informs the client side that rows have been removed from the data source.
+ *
+ * @param firstIndex
+ * the index of the first row removed
+ * @param count
+ * the number of rows removed
+ * @param firstItemId
+ * the item id of the first removed item
+ */
+ private void removeRowData(int firstIndex, int count) {
+ if (clientInitialized) {
+ rpc.removeRowData(firstIndex, count);
+ }
+
+ activeRowHandler.removeRows(firstIndex, count);
+ }
+
+ /**
+ * Informs the client side that data of a row has been modified in the data
+ * source.
+ *
+ * @param itemId
+ * the item Id the row that was updated
+ */
+ public void updateRowData(Object itemId) {
+ if (updatedItemIds.isEmpty()) {
+ // At least one new item will be updated. Mark as dirty to actually
+ // update before response to client.
+ markAsDirty();
+ }
+
+ updatedItemIds.add(itemId);
+ }
+
+ private void internalUpdateRowData(Object itemId) {
+ int index = container.indexOfId(itemId);
+ if (index >= 0) {
+ JsonValue row = getRowData(getGrid().getColumns(), itemId);
+ JsonArray rowArray = Json.createArray();
+ rowArray.set(0, row);
+ rpc.setRowData(index, rowArray);
+ }
+ }
+
+ /**
+ * Pushes a new version of all the rows in the active cache range.
+ */
+ public void refreshCache() {
+ if (!clientInitialized) {
+ return;
+ }
+
+ int firstRow = activeRowHandler.activeRange.getStart();
+ int numberOfRows = activeRowHandler.activeRange.length();
+
+ pushRowData(firstRow, numberOfRows, firstRow, numberOfRows);
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+ super.setParent(parent);
+ if (parent == null) {
+ // We're detached, release various listeners
+
+ activeRowHandler
+ .removeValueChangeListeners(activeRowHandler.activeRange);
+
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .removeItemSetChangeListener(itemListener);
+ }
+
+ } else if (parent instanceof Grid) {
+ Grid grid = (Grid) parent;
+ rowReference = new RowReference(grid);
+ cellReference = new CellReference(rowReference);
+ } else {
+ throw new IllegalStateException(
+ "Grid is the only accepted parent type");
+ }
+ }
+
+ /**
+ * Informs this data provider that given columns have been removed from
+ * grid.
+ *
+ * @param removedColumns
+ * a list of removed columns
+ */
+ public void columnsRemoved(List<Column> removedColumns) {
+ activeRowHandler.columnsRemoved(removedColumns);
+ }
+
+ /**
+ * Informs this data provider that given columns have been added to grid.
+ *
+ * @param addedColumns
+ * a list of added columns
+ */
+ public void columnsAdded(List<Column> addedColumns) {
+ activeRowHandler.columnsAdded(addedColumns);
+ }
+
+ public DataProviderKeyMapper getKeyMapper() {
+ return keyMapper;
+ }
+
+ protected Grid getGrid() {
+ return (Grid) getParent();
+ }
+
+ /**
+ * Converts and encodes the given data model property value using the given
+ * converter and renderer. This method is public only for testing purposes.
+ *
+ * @param renderer
+ * the renderer to use
+ * @param converter
+ * the converter to use
+ * @param modelValue
+ * the value to convert and encode
+ * @param locale
+ * the locale to use in conversion
+ * @return an encoded value ready to be sent to the client
+ */
+ public static <T> JsonValue encodeValue(Object modelValue,
+ Renderer<T> renderer, Converter<?, ?> converter, Locale locale) {
+ Class<T> presentationType = renderer.getPresentationType();
+ T presentationValue;
+
+ if (converter == null) {
+ try {
+ presentationValue = presentationType.cast(modelValue);
+ } catch (ClassCastException e) {
+ ConversionException ee = new Converter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType.getName()
+ + ". No converter is set and the types are not compatible.");
+ if (presentationType == String.class) {
+ // We don't want to throw an exception for the default cause
+ // when one column can't be rendered. Just log the exception
+ // and let the column be empty
+ presentationValue = (T) "";
+ getLogger().log(Level.SEVERE, ee.getMessage(), ee);
+ } else {
+ throw ee;
+ }
+ }
+ } else {
+ assert presentationType.isAssignableFrom(converter
+ .getPresentationType());
+ @SuppressWarnings("unchecked")
+ Converter<T, Object> safeConverter = (Converter<T, Object>) converter;
+ presentationValue = safeConverter.convertToPresentation(modelValue,
+ safeConverter.getPresentationType(), locale);
+ }
+
+ JsonValue encodedValue = renderer.encode(presentationValue);
+
+ return encodedValue;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(RpcDataProviderExtension.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
index 4790d786e7..069cb2e153 100644
--- a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
+++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -344,7 +344,8 @@ public class FieldGroup implements Serializable {
.getWrappedProperty();
}
- if (fieldDataSource == getItemProperty(propertyId)) {
+ if (getItemDataSource() != null
+ && fieldDataSource == getItemProperty(propertyId)) {
if (null != wrapper) {
wrapper.detachFromProperty();
}
diff --git a/server/src/com/vaadin/data/sort/Sort.java b/server/src/com/vaadin/data/sort/Sort.java
new file mode 100644
index 0000000000..81e0d08c73
--- /dev/null
+++ b/server/src/com/vaadin/data/sort/Sort.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.data.sort;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Fluid Sort API. Provides a convenient, human-readable way of specifying
+ * multi-column sort order.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Sort implements Serializable {
+
+ private final Sort previous;
+ private final SortOrder order;
+
+ /**
+ * Initial constructor, called by the static by() methods.
+ *
+ * @param propertyId
+ * a property ID, corresponding to a property in the data source
+ * @param direction
+ * a sort direction value
+ */
+ private Sort(Object propertyId, SortDirection direction) {
+ previous = null;
+ order = new SortOrder(propertyId, direction);
+ }
+
+ /**
+ * Chaining constructor, called by the non-static then() methods. This
+ * constructor links to the previous Sort object.
+ *
+ * @param previous
+ * the sort marker that comes before this one
+ * @param propertyId
+ * a property ID, corresponding to a property in the data source
+ * @param direction
+ * a sort direction value
+ */
+ private Sort(Sort previous, Object propertyId, SortDirection direction) {
+ this.previous = previous;
+ order = new SortOrder(propertyId, direction);
+
+ Sort s = previous;
+ while (s != null) {
+ if (s.order.getPropertyId() == propertyId) {
+ throw new IllegalStateException(
+ "Can not sort along the same property (" + propertyId
+ + ") twice!");
+ }
+ s = s.previous;
+ }
+
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column in ascending
+ * order.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @return a sort object
+ */
+ public static Sort by(Object propertyId) {
+ return by(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @param direction
+ * a sort direction value
+ * @return a sort object
+ */
+ public static Sort by(Object propertyId, SortDirection direction) {
+ return new Sort(propertyId, direction);
+ }
+
+ /**
+ * Continue building a Sort order. The provided property is sorted in
+ * ascending order if the previously added properties have been evaluated as
+ * equals.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @return a sort object
+ */
+ public Sort then(Object propertyId) {
+ return then(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Continue building a Sort order. The provided property is sorted in
+ * specified order if the previously added properties have been evaluated as
+ * equals.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @param direction
+ * a sort direction value
+ * @return a sort object
+ */
+ public Sort then(Object propertyId, SortDirection direction) {
+ return new Sort(this, propertyId, direction);
+ }
+
+ /**
+ * Build a sort order list, ready to be passed to Grid
+ *
+ * @return a sort order list.
+ */
+ public List<SortOrder> build() {
+
+ int count = 1;
+ Sort s = this;
+ while (s.previous != null) {
+ s = s.previous;
+ ++count;
+ }
+
+ List<SortOrder> order = new ArrayList<SortOrder>(count);
+
+ s = this;
+ do {
+ order.add(0, s.order);
+ s = s.previous;
+ } while (s != null);
+
+ return order;
+ }
+}
diff --git a/server/src/com/vaadin/data/sort/SortOrder.java b/server/src/com/vaadin/data/sort/SortOrder.java
new file mode 100644
index 0000000000..2c419f88b7
--- /dev/null
+++ b/server/src/com/vaadin/data/sort/SortOrder.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.data.sort;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Sort order descriptor. Links together a {@link SortDirection} value and a
+ * Vaadin container property ID.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortOrder implements Serializable {
+
+ private final Object propertyId;
+ private final SortDirection direction;
+
+ /**
+ * Create a SortOrder object. Both arguments must be non-null.
+ *
+ * @param propertyId
+ * id of the data source property to sort by
+ * @param direction
+ * value indicating whether the property id should be sorted in
+ * ascending or descending order
+ */
+ public SortOrder(Object propertyId, SortDirection direction) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException("Property ID can not be null!");
+ }
+ if (direction == null) {
+ throw new IllegalArgumentException(
+ "Direction value can not be null!");
+ }
+ this.propertyId = propertyId;
+ this.direction = direction;
+ }
+
+ /**
+ * Returns the property ID.
+ *
+ * @return a property ID
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the {@link SortDirection} value.
+ *
+ * @return a sort direction value
+ */
+ public SortDirection getDirection() {
+ return direction;
+ }
+
+ @Override
+ public String toString() {
+ return propertyId + " " + direction;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + direction.hashCode();
+ result = prime * result + propertyId.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj == null) {
+ return false;
+ } else if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ SortOrder other = (SortOrder) obj;
+ if (direction != other.direction) {
+ return false;
+ } else if (!propertyId.equals(other.propertyId)) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
index fad0934e53..6dcfbb2b84 100644
--- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java
+++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
@@ -226,6 +226,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
@Override
public boolean removeAllItems() {
int origSize = size();
+ IDTYPE firstItem = getFirstVisibleItem();
internalRemoveAllItems();
@@ -238,7 +239,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
// fire event only if the visible view changed, regardless of whether
// filtered out items were removed or not
if (origSize != 0) {
- fireItemSetChange();
+ fireItemsRemoved(0, firstItem, origSize);
}
return true;
@@ -683,6 +684,8 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
protected void addAll(Collection<? extends BEANTYPE> collection)
throws IllegalStateException, IllegalArgumentException {
boolean modified = false;
+ int origSize = size();
+
for (BEANTYPE bean : collection) {
// TODO skipping invalid beans - should not allow them in javadoc?
if (bean == null
@@ -703,13 +706,22 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
if (modified) {
// Filter the contents when all items have been added
if (isFiltered()) {
- filterAll();
- } else {
- fireItemSetChange();
+ doFilterContainer(!getFilters().isEmpty());
+ }
+ if (visibleNewItemsWasAdded(origSize)) {
+ // fire event about added items
+ int firstPosition = origSize;
+ IDTYPE firstItemId = getVisibleItemIds().get(firstPosition);
+ int affectedItems = size() - origSize;
+ fireItemsAdded(firstPosition, firstItemId, affectedItems);
}
}
}
+ private boolean visibleNewItemsWasAdded(int origSize) {
+ return size() > origSize;
+ }
+
/**
* Use the bean resolver to get the identifier for a bean.
*
diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
index 27168694e6..f7b1a4b0d8 100644
--- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
+++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
@@ -15,8 +15,10 @@
*/
package com.vaadin.data.util;
+import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
+import java.util.EventObject;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
@@ -146,6 +148,87 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
}
}
+ private static abstract class BaseItemAddOrRemoveEvent extends EventObject
+ implements Serializable {
+ protected Object itemId;
+ protected int index;
+ protected int count;
+
+ public BaseItemAddOrRemoveEvent(Container source, Object itemId,
+ int index, int count) {
+ super(source);
+ this.itemId = itemId;
+ this.index = index;
+ this.count = count;
+ }
+
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+
+ public Object getFirstItemId() {
+ return itemId;
+ }
+
+ public int getFirstIndex() {
+ return index;
+ }
+
+ public int getAffectedItemsCount() {
+ return count;
+ }
+ }
+
+ /**
+ * An <code>Event</code> object specifying information about the added
+ * items.
+ *
+ * <p>
+ * This class provides information about the first added item and the number
+ * of added items.
+ * </p>
+ *
+ * @since 7.4
+ */
+ protected static class BaseItemAddEvent extends BaseItemAddOrRemoveEvent
+ implements Container.Indexed.ItemAddEvent {
+
+ public BaseItemAddEvent(Container source, Object itemId, int index,
+ int count) {
+ super(source, itemId, index, count);
+ }
+
+ @Override
+ public int getAddedItemsCount() {
+ return getAffectedItemsCount();
+ }
+ }
+
+ /**
+ * An <code>Event</code> object specifying information about the removed
+ * items.
+ *
+ * <p>
+ * This class provides information about the first removed item and the
+ * number of removed items.
+ * </p>
+ *
+ * @since 7.4
+ */
+ protected static class BaseItemRemoveEvent extends BaseItemAddOrRemoveEvent
+ implements Container.Indexed.ItemRemoveEvent {
+
+ public BaseItemRemoveEvent(Container source, Object itemId, int index,
+ int count) {
+ super(source, itemId, index, count);
+ }
+
+ @Override
+ public int getRemovedItemsCount() {
+ return getAffectedItemsCount();
+ }
+ }
+
/**
* Get an item even if filtered out.
*
@@ -897,33 +980,45 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
/**
* Notify item set change listeners that an item has been added to the
* container.
- * <p>
- * Unless subclasses specify otherwise, the default notification indicates a
- * full refresh.
*
* @since 7.4
*
- * @param postion
- * position of the added item in the view (if visible)
+ * @param position
+ * position of the added item in the view
* @param itemId
* id of the added item
* @param item
* the added item
*/
protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) {
- fireItemSetChange();
+ fireItemsAdded(position, itemId, 1);
+ }
+
+ /**
+ * Notify item set change listeners that items has been added to the
+ * container.
+ *
+ * @param firstPosition
+ * position of the first visible added item in the view
+ * @param firstItemId
+ * id of the first visible added item
+ * @param numberOfItems
+ * the number of visible added items
+ */
+ protected void fireItemsAdded(int firstPosition, ITEMIDTYPE firstItemId,
+ int numberOfItems) {
+ BaseItemAddEvent addEvent = new BaseItemAddEvent(this, firstItemId,
+ firstPosition, numberOfItems);
+ fireItemSetChange(addEvent);
}
/**
* Notify item set change listeners that an item has been removed from the
* container.
- * <p>
- * Unless subclasses specify otherwise, the default notification indicates a
- * full refresh.
*
* @since 7.4
*
- * @param postion
+ * @param position
* position of the removed item in the view prior to removal (if
* was visible)
* @param itemId
@@ -931,7 +1026,28 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
* {@link Container#removeItem(Object)} API
*/
protected void fireItemRemoved(int position, Object itemId) {
- fireItemSetChange();
+ fireItemsRemoved(position, itemId, 1);
+ }
+
+ /**
+ * Notify item set change listeners that items has been removed from the
+ * container.
+ *
+ * @param firstPosition
+ * position of the first visible removed item in the view prior
+ * to removal
+ * @param firstItemId
+ * id of the first visible removed item, of type {@link Object}
+ * to satisfy {@link Container#removeItem(Object)} API
+ * @param numberOfItems
+ * the number of removed visible items
+ *
+ */
+ protected void fireItemsRemoved(int firstPosition, Object firstItemId,
+ int numberOfItems) {
+ BaseItemRemoveEvent removeEvent = new BaseItemRemoveEvent(this,
+ firstItemId, firstPosition, numberOfItems);
+ fireItemSetChange(removeEvent);
}
// visible and filtered item identifier lists
@@ -950,6 +1066,23 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
}
/**
+ * Returns the item id of the first visible item after filtering. 'Null' is
+ * returned if there is no visible items.
+ * <p>
+ * For internal use only.
+ *
+ * @since 7.4
+ *
+ * @return item id of the first visible item
+ */
+ protected ITEMIDTYPE getFirstVisibleItem() {
+ if (!getVisibleItemIds().isEmpty()) {
+ return getVisibleItemIds().get(0);
+ }
+ return null;
+ }
+
+ /**
* Returns true is the container has active filters.
*
* @return true if the container is currently filtered
diff --git a/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java b/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java
new file mode 100644
index 0000000000..fd2ce609b8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java
@@ -0,0 +1,724 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.data.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Container wrapper that adds support for generated properties. This container
+ * only supports adding new generated properties. Adding new normal properties
+ * should be done for the wrapped container.
+ *
+ * <p>
+ * Removing properties from this container does not remove anything from the
+ * wrapped container but instead only hides them from the results. These
+ * properties can be returned to this container by calling
+ * {@link #addContainerProperty(Object, Class, Object)} with same property id
+ * which was removed.
+ *
+ * <p>
+ * If wrapped container is Filterable and/or Sortable it should only be handled
+ * through this container as generated properties need to be handled in a
+ * specific way when sorting/filtering.
+ *
+ * <p>
+ * Items returned by this container do not support adding or removing
+ * properties. Generated properties are always read-only. Trying to make them
+ * editable throws an exception.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GeneratedPropertyContainer extends AbstractContainer implements
+ Container.Indexed, Container.Sortable, Container.Filterable,
+ Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier {
+
+ private final Container.Indexed wrappedContainer;
+ private final Map<Object, PropertyValueGenerator<?>> propertyGenerators;
+ private final Map<Filter, List<Filter>> activeFilters;
+ private Sortable sortableContainer = null;
+ private Filterable filterableContainer = null;
+
+ /* Removed properties which are hidden but not actually removed */
+ private final Set<Object> removedProperties = new HashSet<Object>();
+
+ /**
+ * Property implementation for generated properties
+ */
+ protected static class GeneratedProperty<T> implements Property<T> {
+
+ private Item item;
+ private Object itemId;
+ private Object propertyId;
+ private PropertyValueGenerator<T> generator;
+
+ public GeneratedProperty(Item item, Object propertyId, Object itemId,
+ PropertyValueGenerator<T> generator) {
+ this.item = item;
+ this.itemId = itemId;
+ this.propertyId = propertyId;
+ this.generator = generator;
+ }
+
+ @Override
+ public T getValue() {
+ return generator.getValue(item, itemId, propertyId);
+ }
+
+ @Override
+ public void setValue(T newValue) throws ReadOnlyException {
+ throw new ReadOnlyException("Generated properties are read only");
+ }
+
+ @Override
+ public Class<? extends T> getType() {
+ return generator.getType();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (newStatus) {
+ // No-op
+ return;
+ }
+ throw new UnsupportedOperationException(
+ "Generated properties are read only");
+ }
+ }
+
+ /**
+ * Item implementation for generated properties.
+ */
+ protected class GeneratedPropertyItem implements Item {
+
+ private Item wrappedItem;
+ private Object itemId;
+
+ protected GeneratedPropertyItem(Object itemId, Item item) {
+ this.itemId = itemId;
+ wrappedItem = item;
+ }
+
+ @Override
+ public Property getItemProperty(Object id) {
+ if (propertyGenerators.containsKey(id)) {
+ return createProperty(wrappedItem, id, itemId,
+ propertyGenerators.get(id));
+ }
+ return wrappedItem.getItemProperty(id);
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ Set<?> wrappedProperties = asSet(wrappedItem.getItemPropertyIds());
+ return Sets.union(
+ Sets.difference(wrappedProperties, removedProperties),
+ propertyGenerators.keySet());
+ }
+
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "GeneratedPropertyItem does not support adding properties");
+ }
+
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "GeneratedPropertyItem does not support removing properties");
+ }
+ };
+
+ /**
+ * Base implementation for item add or remove events. This is used when an
+ * event is fired from wrapped container and needs to be reconstructed to
+ * act like it actually came from this container.
+ */
+ protected abstract class GeneratedItemAddOrRemoveEvent implements
+ Serializable {
+
+ private Object firstItemId;
+ private int firstIndex;
+ private int count;
+
+ protected GeneratedItemAddOrRemoveEvent(Object itemId, int first,
+ int count) {
+ firstItemId = itemId;
+ firstIndex = first;
+ this.count = count;
+ }
+
+ public Container getContainer() {
+ return GeneratedPropertyContainer.this;
+ }
+
+ public Object getFirstItemId() {
+ return firstItemId;
+ }
+
+ public int getFirstIndex() {
+ return firstIndex;
+ }
+
+ public int getAffectedItemsCount() {
+ return count;
+ }
+ };
+
+ protected class GeneratedItemRemoveEvent extends
+ GeneratedItemAddOrRemoveEvent implements ItemRemoveEvent {
+
+ protected GeneratedItemRemoveEvent(ItemRemoveEvent event) {
+ super(event.getFirstItemId(), event.getFirstIndex(), event
+ .getRemovedItemsCount());
+ }
+
+ @Override
+ public int getRemovedItemsCount() {
+ return super.getAffectedItemsCount();
+ }
+ }
+
+ protected class GeneratedItemAddEvent extends GeneratedItemAddOrRemoveEvent
+ implements ItemAddEvent {
+
+ protected GeneratedItemAddEvent(ItemAddEvent event) {
+ super(event.getFirstItemId(), event.getFirstIndex(), event
+ .getAddedItemsCount());
+ }
+
+ @Override
+ public int getAddedItemsCount() {
+ return super.getAffectedItemsCount();
+ }
+
+ }
+
+ /**
+ * Constructor for GeneratedPropertyContainer.
+ *
+ * @param container
+ * underlying indexed container
+ */
+ public GeneratedPropertyContainer(Container.Indexed container) {
+ wrappedContainer = container;
+ propertyGenerators = new HashMap<Object, PropertyValueGenerator<?>>();
+
+ if (wrappedContainer instanceof Sortable) {
+ sortableContainer = (Sortable) wrappedContainer;
+ }
+
+ if (wrappedContainer instanceof Filterable) {
+ activeFilters = new HashMap<Filter, List<Filter>>();
+ filterableContainer = (Filterable) wrappedContainer;
+ } else {
+ activeFilters = null;
+ }
+
+ // ItemSetChangeEvents
+ if (wrappedContainer instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) wrappedContainer)
+ .addItemSetChangeListener(new ItemSetChangeListener() {
+
+ @Override
+ public void containerItemSetChange(
+ ItemSetChangeEvent event) {
+ if (event instanceof ItemAddEvent) {
+ final ItemAddEvent addEvent = (ItemAddEvent) event;
+ fireItemSetChange(new GeneratedItemAddEvent(
+ addEvent));
+ } else if (event instanceof ItemRemoveEvent) {
+ final ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
+ fireItemSetChange(new GeneratedItemRemoveEvent(
+ removeEvent));
+ } else {
+ fireItemSetChange();
+ }
+ }
+ });
+ }
+
+ // PropertySetChangeEvents
+ if (wrappedContainer instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) wrappedContainer)
+ .addPropertySetChangeListener(new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(
+ PropertySetChangeEvent event) {
+ fireContainerPropertySetChange();
+ }
+ });
+ }
+ }
+
+ /* Functions related to generated properties */
+
+ /**
+ * Add a new PropertyValueGenerator with given property id. This will
+ * override any existing properties with the same property id. Fires a
+ * PropertySetChangeEvent.
+ *
+ * @param propertyId
+ * property id
+ * @param generator
+ * a property value generator
+ */
+ public void addGeneratedProperty(Object propertyId,
+ PropertyValueGenerator<?> generator) {
+ propertyGenerators.put(propertyId, generator);
+ fireContainerPropertySetChange();
+ }
+
+ /**
+ * Removes any possible PropertyValueGenerator with given property id. Fires
+ * a PropertySetChangeEvent.
+ *
+ * @param propertyId
+ * property id
+ */
+ public void removeGeneratedProperty(Object propertyId) {
+ if (propertyGenerators.containsKey(propertyId)) {
+ propertyGenerators.remove(propertyId);
+ fireContainerPropertySetChange();
+ }
+ }
+
+ private Item createGeneratedPropertyItem(final Object itemId,
+ final Item item) {
+ return new GeneratedPropertyItem(itemId, item);
+ }
+
+ private <T> Property<T> createProperty(final Item item,
+ final Object propertyId, final Object itemId,
+ final PropertyValueGenerator<T> generator) {
+ return new GeneratedProperty<T>(item, propertyId, itemId, generator);
+ }
+
+ private static <T> LinkedHashSet<T> asSet(Collection<T> collection) {
+ if (collection instanceof LinkedHashSet) {
+ return (LinkedHashSet<T>) collection;
+ } else {
+ return new LinkedHashSet<T>(collection);
+ }
+ }
+
+ /* Listener functionality */
+
+ @Override
+ public void addItemSetChangeListener(ItemSetChangeListener listener) {
+ super.addItemSetChangeListener(listener);
+ }
+
+ @Override
+ public void addListener(ItemSetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeItemSetChangeListener(ItemSetChangeListener listener) {
+ super.removeItemSetChangeListener(listener);
+ }
+
+ @Override
+ public void removeListener(ItemSetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ @Override
+ public void addPropertySetChangeListener(PropertySetChangeListener listener) {
+ super.addPropertySetChangeListener(listener);
+ }
+
+ @Override
+ public void addListener(PropertySetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removePropertySetChangeListener(
+ PropertySetChangeListener listener) {
+ super.removePropertySetChangeListener(listener);
+ }
+
+ @Override
+ public void removeListener(PropertySetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ /* Filtering functionality */
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ if (filterableContainer == null) {
+ throw new UnsupportedOperationException(
+ "Wrapped container is not filterable");
+ }
+
+ List<Filter> addedFilters = new ArrayList<Filter>();
+ for (Entry<?, PropertyValueGenerator<?>> entry : propertyGenerators
+ .entrySet()) {
+ Object property = entry.getKey();
+ if (filter.appliesToProperty(property)) {
+ // Have generated property modify filter to fit the original
+ // data in the container.
+ Filter modifiedFilter = entry.getValue().modifyFilter(filter);
+ filterableContainer.addContainerFilter(modifiedFilter);
+ // Keep track of added filters
+ addedFilters.add(modifiedFilter);
+ }
+ }
+
+ if (addedFilters.isEmpty()) {
+ // No generated property modified this filter, use it as is
+ addedFilters.add(filter);
+ filterableContainer.addContainerFilter(filter);
+ }
+ // Map filter to actually added filters
+ activeFilters.put(filter, addedFilters);
+ }
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ if (filterableContainer == null) {
+ throw new UnsupportedOperationException(
+ "Wrapped container is not filterable");
+ }
+
+ if (activeFilters.containsKey(filter)) {
+ for (Filter f : activeFilters.get(filter)) {
+ filterableContainer.removeContainerFilter(f);
+ }
+ activeFilters.remove(filter);
+ }
+ }
+
+ @Override
+ public void removeAllContainerFilters() {
+ if (filterableContainer == null) {
+ throw new UnsupportedOperationException(
+ "Wrapped container is not filterable");
+ }
+ filterableContainer.removeAllContainerFilters();
+ activeFilters.clear();
+ }
+
+ @Override
+ public Collection<Filter> getContainerFilters() {
+ if (filterableContainer == null) {
+ throw new UnsupportedOperationException(
+ "Wrapped container is not filterable");
+ }
+ return Collections.unmodifiableSet(activeFilters.keySet());
+ }
+
+ /* Sorting functionality */
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ if (sortableContainer == null) {
+ new UnsupportedOperationException(
+ "Wrapped container is not Sortable");
+ }
+
+ if (propertyId.length == 0) {
+ sortableContainer.sort(propertyId, ascending);
+ return;
+ }
+
+ List<Object> actualSortProperties = new ArrayList<Object>();
+ List<Boolean> actualSortDirections = new ArrayList<Boolean>();
+
+ for (int i = 0; i < propertyId.length; ++i) {
+ Object property = propertyId[i];
+ SortDirection direction;
+ boolean isAscending = i < ascending.length ? ascending[i] : true;
+ if (isAscending) {
+ direction = SortDirection.ASCENDING;
+ } else {
+ direction = SortDirection.DESCENDING;
+ }
+
+ if (propertyGenerators.containsKey(property)) {
+ // Sorting by a generated property. Generated property should
+ // modify sort orders to work with original properties in the
+ // container.
+ for (SortOrder s : propertyGenerators.get(property)
+ .getSortProperties(new SortOrder(property, direction))) {
+ actualSortProperties.add(s.getPropertyId());
+ actualSortDirections
+ .add(s.getDirection() == SortDirection.ASCENDING);
+ }
+ } else {
+ actualSortProperties.add(property);
+ actualSortDirections.add(isAscending);
+ }
+ }
+
+ boolean[] actualAscending = new boolean[actualSortDirections.size()];
+ for (int i = 0; i < actualAscending.length; ++i) {
+ actualAscending[i] = actualSortDirections.get(i);
+ }
+
+ sortableContainer.sort(actualSortProperties.toArray(), actualAscending);
+ }
+
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ if (sortableContainer == null) {
+ new UnsupportedOperationException(
+ "Wrapped container is not Sortable");
+ }
+
+ Set<Object> sortablePropertySet = new HashSet<Object>(
+ sortableContainer.getSortableContainerPropertyIds());
+ for (Entry<?, PropertyValueGenerator<?>> entry : propertyGenerators
+ .entrySet()) {
+ Object property = entry.getKey();
+ SortOrder order = new SortOrder(property, SortDirection.ASCENDING);
+ if (entry.getValue().getSortProperties(order).length > 0) {
+ sortablePropertySet.add(property);
+ } else {
+ sortablePropertySet.remove(property);
+ }
+ }
+
+ return sortablePropertySet;
+ }
+
+ /* Item related overrides */
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ Item item = wrappedContainer.addItemAfter(previousItemId, newItemId);
+ return createGeneratedPropertyItem(newItemId, item);
+ }
+
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ Item item = wrappedContainer.addItem(itemId);
+ return createGeneratedPropertyItem(itemId, item);
+ }
+
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ Item item = wrappedContainer.addItemAt(index, newItemId);
+ return createGeneratedPropertyItem(newItemId, item);
+ }
+
+ @Override
+ public Item getItem(Object itemId) {
+ Item item = wrappedContainer.getItem(itemId);
+ return createGeneratedPropertyItem(itemId, item);
+ }
+
+ /* Property related overrides */
+
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ if (propertyGenerators.keySet().contains(propertyId)) {
+ return getItem(itemId).getItemProperty(propertyId);
+ } else if (!removedProperties.contains(propertyId)) {
+ return wrappedContainer.getContainerProperty(itemId, propertyId);
+ }
+ return null;
+ }
+
+ /**
+ * Returns a list of propety ids available in this container. This
+ * collection will contain properties for generated properties. Removed
+ * properties will not show unless there is a generated property overriding
+ * those.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ Set<?> wrappedProperties = asSet(wrappedContainer
+ .getContainerPropertyIds());
+ return Sets.union(
+ Sets.difference(wrappedProperties, removedProperties),
+ propertyGenerators.keySet());
+ }
+
+ /**
+ * Adds a previously removed property back to GeneratedPropertyContainer.
+ * Adding a property that is not previously removed causes an
+ * UnsupportedOperationException.
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ if (!removedProperties.contains(propertyId)) {
+ throw new UnsupportedOperationException(
+ "GeneratedPropertyContainer does not support adding properties.");
+ }
+ removedProperties.remove(propertyId);
+ fireContainerPropertySetChange();
+ return true;
+ }
+
+ /**
+ * Marks the given property as hidden. This property from wrapped container
+ * will be removed from {@link #getContainerPropertyIds()} and is no longer
+ * be available in Items retrieved from this container.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ if (wrappedContainer.getContainerPropertyIds().contains(propertyId)
+ && removedProperties.add(propertyId)) {
+ fireContainerPropertySetChange();
+ return true;
+ }
+ return false;
+ }
+
+ /* Type related overrides */
+
+ @Override
+ public Class<?> getType(Object propertyId) {
+ if (propertyGenerators.containsKey(propertyId)) {
+ return propertyGenerators.get(propertyId).getType();
+ } else {
+ return wrappedContainer.getType(propertyId);
+ }
+ }
+
+ /* Unmodified functions */
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return wrappedContainer.nextItemId(itemId);
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return wrappedContainer.prevItemId(itemId);
+ }
+
+ @Override
+ public Object firstItemId() {
+ return wrappedContainer.firstItemId();
+ }
+
+ @Override
+ public Object lastItemId() {
+ return wrappedContainer.lastItemId();
+ }
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ return wrappedContainer.isFirstId(itemId);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return wrappedContainer.isLastId(itemId);
+ }
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ return wrappedContainer.addItemAfter(previousItemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return wrappedContainer.getItemIds();
+ }
+
+ @Override
+ public int size() {
+ return wrappedContainer.size();
+ }
+
+ @Override
+ public boolean containsId(Object itemId) {
+ return wrappedContainer.containsId(itemId);
+ }
+
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ return wrappedContainer.addItem();
+ }
+
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ return wrappedContainer.removeItem(itemId);
+ }
+
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ return wrappedContainer.removeAllItems();
+ }
+
+ @Override
+ public int indexOfId(Object itemId) {
+ return wrappedContainer.indexOfId(itemId);
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return wrappedContainer.getIdByIndex(index);
+ }
+
+ @Override
+ public List<?> getItemIds(int startIndex, int numberOfItems) {
+ return wrappedContainer.getItemIds(startIndex, numberOfItems);
+ }
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ return wrappedContainer.addItemAt(index);
+ }
+
+ /**
+ * Returns the original underlying container.
+ *
+ * @return the original underlying container
+ */
+ public Container.Indexed getWrappedContainer() {
+ return wrappedContainer;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java
index 68960335d7..b851baf674 100644
--- a/server/src/com/vaadin/data/util/IndexedContainer.java
+++ b/server/src/com/vaadin/data/util/IndexedContainer.java
@@ -226,6 +226,7 @@ public class IndexedContainer extends
@Override
public boolean removeAllItems() {
int origSize = size();
+ Object firstItem = getFirstVisibleItem();
internalRemoveAllItems();
@@ -235,16 +236,17 @@ public class IndexedContainer extends
// filtered out items were removed or not
if (origSize != 0) {
// Sends a change event
- fireItemSetChange();
+ fireItemsRemoved(0, firstItem, origSize);
}
return true;
}
- /*
- * (non-Javadoc)
- *
- * @see com.vaadin.data.Container#addItem()
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The item ID is generated from a sequence of Integers. The id of the first
+ * added item is 1.
*/
@Override
public Object addItem() {
@@ -362,10 +364,11 @@ public class IndexedContainer extends
new IndexedContainerItem(newItemId), true);
}
- /*
- * (non-Javadoc)
- *
- * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The item ID is generated from a sequence of Integers. The id of the first
+ * added item is 1.
*/
@Override
public Object addItemAfter(Object previousItemId) {
@@ -391,10 +394,11 @@ public class IndexedContainer extends
newItemId), true);
}
- /*
- * (non-Javadoc)
- *
- * @see com.vaadin.data.Container.Indexed#addItemAt(int)
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The item ID is generated from a sequence of Integers. The id of the first
+ * added item is 1.
*/
@Override
public Object addItemAt(int index) {
@@ -620,8 +624,7 @@ public class IndexedContainer extends
@Override
protected void fireItemAdded(int position, Object itemId, Item item) {
if (position >= 0) {
- fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this,
- position));
+ super.fireItemAdded(position, itemId, item);
}
}
@@ -1211,4 +1214,5 @@ public class IndexedContainer extends
public Collection<Filter> getContainerFilters() {
return super.getContainerFilters();
}
+
}
diff --git a/server/src/com/vaadin/data/util/PropertyValueGenerator.java b/server/src/com/vaadin/data/util/PropertyValueGenerator.java
new file mode 100644
index 0000000000..453e45b1db
--- /dev/null
+++ b/server/src/com/vaadin/data/util/PropertyValueGenerator.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.data.util;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * PropertyValueGenerator for GeneratedPropertyContainer.
+ *
+ * @param <T>
+ * Property data type
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public abstract class PropertyValueGenerator<T> implements Serializable {
+
+ /**
+ * Returns value for given Item. Used by GeneratedPropertyContainer when
+ * generating new properties.
+ *
+ * @param item
+ * currently handled item
+ * @param itemId
+ * item id for currently handled item
+ * @param propertyId
+ * id for this property
+ * @return generated value
+ */
+ public abstract T getValue(Item item, Object itemId, Object propertyId);
+
+ /**
+ * Return Property type for this generator. This function is called when
+ * {@link Property#getType()} is called for generated property.
+ *
+ * @return type of generated property
+ */
+ public abstract Class<T> getType();
+
+ /**
+ * Translates sorting of the generated property in a specific direction to a
+ * set of property ids and directions in the underlying container.
+ *
+ * SortOrder is similar to (or the same as) the SortOrder already defined
+ * for Grid.
+ *
+ * The default implementation of this method returns an empty array, which
+ * means that the property will not be included in
+ * getSortableContainerPropertyIds(). Attempting to sort by that column
+ * throws UnsupportedOperationException.
+ *
+ * Returning null is not allowed.
+ *
+ * @param order
+ * a sort order for this property
+ * @return an array of sort orders describing how this property is sorted
+ */
+ public SortOrder[] getSortProperties(SortOrder order) {
+ return new SortOrder[] {};
+ }
+
+ /**
+ * Return an updated filter that should be compatible with the underlying
+ * container.
+ *
+ * This function is called when setting a filter for this generated
+ * property. Returning null from this function causes
+ * GeneratedPropertyContainer to discard the filter and not use it.
+ *
+ * By default this function throws UnsupportedFilterException.
+ *
+ * @param filter
+ * original filter for this property
+ * @return modified filter that is compatible with the underlying container
+ * @throws UnsupportedFilterException
+ */
+ public Filter modifyFilter(Filter filter) throws UnsupportedFilterException {
+ throw new UnsupportedFilterException("Filter" + filter
+ + " is not supported");
+ }
+
+}
diff --git a/server/src/com/vaadin/event/SelectionEvent.java b/server/src/com/vaadin/event/SelectionEvent.java
new file mode 100644
index 0000000000..e75369e6da
--- /dev/null
+++ b/server/src/com/vaadin/event/SelectionEvent.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.event;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+
+/**
+ * An event that specifies what in a selection has changed, and where the
+ * selection took place.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SelectionEvent extends EventObject {
+
+ private LinkedHashSet<Object> oldSelection;
+ private LinkedHashSet<Object> newSelection;
+
+ public SelectionEvent(Object source, Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ super(source);
+ this.oldSelection = new LinkedHashSet<Object>(oldSelection);
+ this.newSelection = new LinkedHashSet<Object>(newSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that became selected.
+ * <p>
+ * <em>Note:</em> this excludes all itemIds that might have been previously
+ * selected.
+ *
+ * @return a Collection of the itemIds that became selected
+ */
+ public Set<Object> getAdded() {
+ return Sets.difference(newSelection, oldSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that became deselected.
+ * <p>
+ * <em>Note:</em> this excludes all itemIds that might have been previously
+ * deselected.
+ *
+ * @return a Collection of the itemIds that became deselected
+ */
+ public Set<Object> getRemoved() {
+ return Sets.difference(oldSelection, newSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that are currently selected.
+ *
+ * @return a Collection of the itemIds that are currently selected
+ */
+ public Set<Object> getSelected() {
+ return Collections.unmodifiableSet(newSelection);
+ }
+
+ /**
+ * The listener interface for receiving {@link SelectionEvent
+ * SelectionEvents}.
+ */
+ public interface SelectionListener extends Serializable {
+ /**
+ * Notifies the listener that the selection state has changed.
+ *
+ * @param event
+ * the selection change event
+ */
+ void select(SelectionEvent event);
+ }
+
+ /**
+ * The interface for adding and removing listeners for
+ * {@link SelectionEvent SelectionEvents}.
+ */
+ public interface SelectionNotifier extends Serializable {
+ /**
+ * Registers a new selection listener
+ *
+ * @param listener
+ * the listener to register
+ */
+ void addSelectionListener(SelectionListener listener);
+
+ /**
+ * Removes a previously registered selection change listener
+ *
+ * @param listener
+ * the listener to remove
+ */
+ void removeSelectionListener(SelectionListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/event/SortEvent.java b/server/src/com/vaadin/event/SortEvent.java
new file mode 100644
index 0000000000..f303e47781
--- /dev/null
+++ b/server/src/com/vaadin/event/SortEvent.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.event;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.ui.Component;
+
+/**
+ * Event describing a change in sorting of a {@link Container}. Fired by
+ * {@link SortNotifier SortNotifiers}.
+ *
+ * @see SortListener
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortEvent extends Component.Event {
+
+ private final List<SortOrder> sortOrder;
+ private final boolean userOriginated;
+
+ /**
+ * Creates a new sort order change event with a sort order list.
+ *
+ * @param source
+ * the component from which the event originates
+ * @param sortOrder
+ * the new sort order list
+ * @param userOriginated
+ * <code>true</code> if event is a result of user interaction,
+ * <code>false</code> if from API call
+ */
+ public SortEvent(Component source, List<SortOrder> sortOrder,
+ boolean userOriginated) {
+ super(source);
+ this.sortOrder = sortOrder;
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Gets the sort order list.
+ *
+ * @return the sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return sortOrder;
+ }
+
+ /**
+ * Returns whether this event originated from actions done by the user.
+ *
+ * @return true if sort event originated from user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ /**
+ * Listener for sort order change events.
+ */
+ public interface SortListener extends Serializable {
+ /**
+ * Called when the sort order has changed.
+ *
+ * @param event
+ * the sort order change event
+ */
+ public void sort(SortEvent event);
+ }
+
+ /**
+ * The interface for adding and removing listeners for {@link SortEvent
+ * SortEvents}.
+ */
+ public interface SortNotifier extends Serializable {
+ /**
+ * Adds a sort order change listener that gets notified when the sort
+ * order changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ public void addSortListener(SortListener listener);
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortListener(SortListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ public void removeSortListener(SortListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/server/AbstractJavaScriptExtension.java b/server/src/com/vaadin/server/AbstractJavaScriptExtension.java
index e182319c85..e9cf2c5e33 100644
--- a/server/src/com/vaadin/server/AbstractJavaScriptExtension.java
+++ b/server/src/com/vaadin/server/AbstractJavaScriptExtension.java
@@ -106,8 +106,8 @@ import com.vaadin.ui.JavaScriptFunction;
* <li>Java Dates are represented by JavaScript numbers containing the timestamp
* </li>
* <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li>
- * <li>Map<String, ?> in Java is represented by JavaScript object with fields
- * corresponding to the map keys.</li>
+ * <li>Map&lt;String, ?&gt; in Java is represented by JavaScript object with
+ * fields corresponding to the map keys.</li>
* <li>Any other Java Map is represented by a JavaScript array containing two
* arrays, the first contains the keys and the second contains the values in the
* same order.</li>
diff --git a/server/src/com/vaadin/server/JsonCodec.java b/server/src/com/vaadin/server/JsonCodec.java
index 6833cef1f7..498bf3e6b2 100644
--- a/server/src/com/vaadin/server/JsonCodec.java
+++ b/server/src/com/vaadin/server/JsonCodec.java
@@ -300,7 +300,9 @@ public class JsonCodec implements Serializable {
}
// Try to decode object using fields
- if (value.getType() == JsonType.NULL) {
+ if (isJsonType(targetType)) {
+ return value;
+ } else if (value.getType() == JsonType.NULL) {
return null;
} else if (targetType == byte.class || targetType == Byte.class) {
return Byte.valueOf((byte) value.asNumber());
@@ -334,6 +336,11 @@ public class JsonCodec implements Serializable {
}
}
+ private static boolean isJsonType(Type type) {
+ return type instanceof Class<?>
+ && JsonValue.class.isAssignableFrom((Class<?>) type);
+ }
+
private static Object decodeArray(Type componentType, JsonArray value,
ConnectorTracker connectorTracker) {
Class<?> componentClass = getClassForType(componentType);
diff --git a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java
index f3cbf47b62..84023555bb 100644
--- a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java
+++ b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java
@@ -119,8 +119,8 @@ import com.vaadin.shared.ui.JavaScriptComponentState;
* <li>Java Dates are represented by JavaScript numbers containing the timestamp
* </li>
* <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li>
- * <li>Map<String, ?> in Java is represented by JavaScript object with fields
- * corresponding to the map keys.</li>
+ * <li>Map&lt;String, ?&gt; in Java is represented by JavaScript object with
+ * fields corresponding to the map keys.</li>
* <li>Any other Java Map is represented by a JavaScript array containing two
* arrays, the first contains the keys and the second contains the values in the
* same order.</li>
diff --git a/server/src/com/vaadin/ui/DefaultFieldFactory.java b/server/src/com/vaadin/ui/DefaultFieldFactory.java
index ad6461686c..535943bcd5 100644
--- a/server/src/com/vaadin/ui/DefaultFieldFactory.java
+++ b/server/src/com/vaadin/ui/DefaultFieldFactory.java
@@ -20,6 +20,7 @@ import java.util.Date;
import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
+import com.vaadin.shared.util.SharedUtil;
/**
* This class contains a basic implementation for both {@link FormFieldFactory}
@@ -75,42 +76,7 @@ public class DefaultFieldFactory implements FormFieldFactory, TableFieldFactory
* @return the formatted caption string
*/
public static String createCaptionByPropertyId(Object propertyId) {
- String name = propertyId.toString();
- if (name.length() > 0) {
-
- int dotLocation = name.lastIndexOf('.');
- if (dotLocation > 0 && dotLocation < name.length() - 1) {
- name = name.substring(dotLocation + 1);
- }
- if (name.indexOf(' ') < 0
- && name.charAt(0) == Character.toLowerCase(name.charAt(0))
- && name.charAt(0) != Character.toUpperCase(name.charAt(0))) {
- StringBuffer out = new StringBuffer();
- out.append(Character.toUpperCase(name.charAt(0)));
- int i = 1;
-
- while (i < name.length()) {
- int j = i;
- for (; j < name.length(); j++) {
- char c = name.charAt(j);
- if (Character.toLowerCase(c) != c
- && Character.toUpperCase(c) == c) {
- break;
- }
- }
- if (j == name.length()) {
- out.append(name.substring(i));
- } else {
- out.append(name.substring(i, j));
- out.append(" " + name.charAt(j));
- }
- i = j + 1;
- }
-
- name = out.toString();
- }
- }
- return name;
+ return SharedUtil.propertyIdToHumanFriendly(propertyId);
}
/**
diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java
new file mode 100644
index 0000000000..adb08e0fcc
--- /dev/null
+++ b/server/src/com/vaadin/ui/Grid.java
@@ -0,0 +1,4678 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.event.SelectionEvent.SelectionNotifier;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.event.SortEvent.SortNotifier;
+import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.ErrorMessage;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.EditorClientRpc;
+import com.vaadin.shared.ui.grid.EditorServerRpc;
+import com.vaadin.shared.ui.grid.GridClientRpc;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.renderer.ObjectRenderer;
+import com.vaadin.ui.renderer.Renderer;
+import com.vaadin.util.ReflectTools;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * A grid component for displaying tabular data.
+ * <p>
+ * Grid is always bound to a {@link Container.Indexed}, but is not a
+ * {@code Container} of any kind in of itself. The contents of the given
+ * Container is displayed with the help of {@link Renderer Renderers}.
+ *
+ * <h3 id="grid-headers-and-footers">Headers and Footers</h3>
+ * <p>
+ *
+ *
+ * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
+ * <p>
+ * Each column has its own {@link Renderer} that displays data into something
+ * that can be displayed in the browser. That data is first converted with a
+ * {@link com.vaadin.data.util.converter.Converter Converter} into something
+ * that the Renderer can process. This can also be an implicit step - if a
+ * column has a simple data type, like a String, no explicit assignment is
+ * needed.
+ * <p>
+ * Usually a renderer takes some kind of object, and converts it into a
+ * HTML-formatted string.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ * Column column = grid.getColumn(STRING_DATE_PROPERTY);
+ * column.setConverter(new StringToDateConverter());
+ * column.setRenderer(new MyColorfulDateRenderer());
+ * </pre></code>
+ *
+ * <h3 id="grid-lazyloading">Lazy Loading</h3>
+ * <p>
+ * The data is accessed as it is needed by Grid and not any sooner. In other
+ * words, if the given Container is huge, but only the first few rows are
+ * displayed to the user, only those (and a few more, for caching purposes) are
+ * accessed.
+ *
+ * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
+ * <p>
+ * Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
+ * multi, none), and comes bundled with one
+ * <em>{@link SelectionModel model}</em> for each of the modes. The distinction
+ * between a selection mode and selection model is as follows: a <em>mode</em>
+ * essentially says whether you can have one, many or no rows selected. The
+ * model, however, has the behavioral details of each. A single selection model
+ * may require that the user deselects one row before selecting another one. A
+ * variant of a multiselect might have a configurable maximum of rows that may
+ * be selected. And so on.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ *
+ * // uses the bundled SingleSelectionModel class
+ * grid.setSelectionMode(SelectionMode.SINGLE);
+ *
+ * // changes the behavior to a custom selection model
+ * grid.setSelectionModel(new MyTwoSelectionModel());
+ * </pre></code>
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Grid extends AbstractComponent implements SelectionNotifier,
+ SortNotifier, SelectiveRenderer, ItemClickNotifier {
+
+ /**
+ * Custom field group that allows finding property types before an item has
+ * been bound.
+ */
+ private final class CustomFieldGroup extends FieldGroup {
+ @Override
+ protected Class<?> getPropertyType(Object propertyId)
+ throws BindException {
+ if (getItemDataSource() == null) {
+ return datasource.getType(propertyId);
+ } else {
+ return super.getPropertyType(propertyId);
+ }
+ }
+ }
+
+ /**
+ * Selection modes representing built-in {@link SelectionModel
+ * SelectionModels} that come bundled with {@link Grid}.
+ * <p>
+ * Passing one of these enums into
+ * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
+ * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
+ * implementations of {@link SelectionModel}.
+ *
+ * @see Grid#setSelectionMode(SelectionMode)
+ * @see Grid#setSelectionModel(SelectionModel)
+ */
+ public enum SelectionMode {
+ /** A SelectionMode that maps to {@link SingleSelectionModel} */
+ SINGLE {
+ @Override
+ protected SelectionModel createModel() {
+ return new SingleSelectionModel();
+ }
+
+ },
+
+ /** A SelectionMode that maps to {@link MultiSelectionModel} */
+ MULTI {
+ @Override
+ protected SelectionModel createModel() {
+ return new MultiSelectionModel();
+ }
+ },
+
+ /** A SelectionMode that maps to {@link NoSelectionModel} */
+ NONE {
+ @Override
+ protected SelectionModel createModel() {
+ return new NoSelectionModel();
+ }
+ };
+
+ protected abstract SelectionModel createModel();
+ }
+
+ /**
+ * The server-side interface that controls Grid's selection state.
+ */
+ public interface SelectionModel extends Serializable {
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ boolean isSelected(Object itemId);
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ Collection<Object> getSelectedRows();
+
+ /**
+ * Injects the current {@link Grid} instance into the SelectionModel.
+ * <p>
+ * <em>Note:</em> This method should not be called manually.
+ *
+ * @param grid
+ * the Grid in which the SelectionModel currently is, or
+ * <code>null</code> when a selection model is being detached
+ * from a Grid.
+ */
+ void setGrid(Grid grid);
+
+ /**
+ * Resets the SelectiomModel to an initial state.
+ * <p>
+ * Most often this means that the selection state is cleared, but
+ * implementations are free to interpret the "initial state" as they
+ * wish. Some, for example, may want to keep the first selected item as
+ * selected.
+ */
+ void reset();
+
+ /**
+ * A SelectionModel that supports multiple selections to be made.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Multi extends SelectionModel {
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemId(s) to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ * @see #deselect(Object...)
+ */
+ boolean select(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ * @see #deselect(Collection)
+ */
+ boolean select(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #select(Object...)
+ */
+ boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #select(Collection)
+ */
+ boolean deselect(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks all the items in the current Container as selected
+ *
+ * @return <code>true</code> iff some items were previously not
+ * selected
+ * @see #deselectAll()
+ */
+ boolean selectAll();
+
+ /**
+ * Marks all the items in the current Container as deselected
+ *
+ * @return <code>true</code> iff some items were previously selected
+ * @see #selectAll()
+ */
+ boolean deselectAll();
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * given Collection.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ */
+ boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * varargs array.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ */
+ boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException;
+ }
+
+ /**
+ * A SelectionModel that supports for only single rows to be selected at
+ * a time.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Single extends SelectionModel {
+
+ /**
+ * Marks an item as selected.
+ *
+ * @param itemIds
+ * the itemId to mark as selected; <code>null</code> for
+ * deselect
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might
+ * be that the given id was null, indicating a deselect,
+ * but implementation doesn't allow deselecting.
+ * re-selecting something
+ * @throws IllegalArgumentException
+ * if given itemId does not exist in the container of
+ * Grid
+ */
+ boolean select(Object itemId) throws IllegalStateException,
+ IllegalArgumentException;
+
+ /**
+ * Gets the item id of the currently selected item.
+ *
+ * @return the item id of the currently selected item, or
+ * <code>null</code> if nothing is selected
+ */
+ Object getSelectedRow();
+ }
+
+ /**
+ * A SelectionModel that does not allow for rows to be selected.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if the
+ * developer is unable to select something programmatically, it is not
+ * allowed for the end-user to select anything, either.
+ */
+ public interface None extends SelectionModel {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always <code>false</code>.
+ */
+ @Override
+ public boolean isSelected(Object itemId);
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always an empty collection.
+ */
+ @Override
+ public Collection<Object> getSelectedRows();
+ }
+ }
+
+ /**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ */
+ public static abstract class AbstractSelectionModel implements
+ SelectionModel {
+ protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>();
+ protected Grid grid = null;
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return selection.contains(itemId);
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return new ArrayList<Object>(selection);
+ }
+
+ @Override
+ public void setGrid(final Grid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sanity check for existence of item id.
+ *
+ * @param itemId
+ * item id to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if item Id doesn't exist in the container of Grid
+ */
+ protected void checkItemIdExists(Object itemId)
+ throws IllegalArgumentException {
+ if (!grid.getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Given item id (" + itemId
+ + ") does not exist in the container");
+ }
+ }
+
+ /**
+ * Sanity check for existence of item ids in given collection.
+ *
+ * @param itemIds
+ * item id collection to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if at least one item id doesn't exist in the container of
+ * Grid
+ */
+ protected void checkItemIdsExist(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ for (Object itemId : itemIds) {
+ checkItemIdExists(itemId);
+ }
+ }
+
+ /**
+ * Fires a {@link SelectionEvent} to all the {@link SelectionListener
+ * SelectionListeners} currently added to the Grid in which this
+ * SelectionModel is.
+ * <p>
+ * Note that this is only a helper method, and routes the call all the
+ * way to Grid. A {@link SelectionModel} is not a
+ * {@link SelectionNotifier}
+ *
+ * @param oldSelection
+ * the complete {@link Collection} of the itemIds that were
+ * selected <em>before</em> this event happened
+ * @param newSelection
+ * the complete {@link Collection} of the itemIds that are
+ * selected <em>after</em> this event happened
+ */
+ protected void fireSelectionEvent(
+ final Collection<Object> oldSelection,
+ final Collection<Object> newSelection) {
+ grid.fireSelectionEvent(oldSelection, newSelection);
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Single}
+ */
+ public static class SingleSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Single {
+ @Override
+ public boolean select(final Object itemId) {
+ if (itemId == null) {
+ return deselect(getSelectedRow());
+ }
+
+ checkItemIdExists(itemId);
+
+ final Object selectedRow = getSelectedRow();
+ final boolean modified = selection.add(itemId);
+ if (modified) {
+ final Collection<Object> deselected;
+ if (selectedRow != null) {
+ deselectInternal(selectedRow, false);
+ deselected = Collections.singleton(selectedRow);
+ } else {
+ deselected = Collections.emptySet();
+ }
+
+ fireSelectionEvent(deselected, selection);
+ }
+
+ return modified;
+ }
+
+ private boolean deselect(final Object itemId) {
+ return deselectInternal(itemId, true);
+ }
+
+ private boolean deselectInternal(final Object itemId,
+ boolean fireEventIfNeeded) {
+ final boolean modified = selection.remove(itemId);
+ if (fireEventIfNeeded && modified) {
+ fireSelectionEvent(Collections.singleton(itemId),
+ Collections.emptySet());
+ }
+ return modified;
+ }
+
+ @Override
+ public Object getSelectedRow() {
+ if (selection.isEmpty()) {
+ return null;
+ } else {
+ return selection.iterator().next();
+ }
+ }
+
+ /**
+ * Resets the selection state.
+ * <p>
+ * If an item is selected, it will become deselected.
+ */
+ @Override
+ public void reset() {
+ deselect(getSelectedRow());
+ }
+ }
+
+ /**
+ * A default implementation for a {@link SelectionModel.None}
+ */
+ public static class NoSelectionModel implements SelectionModel.None {
+ @Override
+ public void setGrid(final Grid grid) {
+ // NOOP, not needed for anything
+ }
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return false;
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Semantically resets the selection model.
+ * <p>
+ * Effectively a no-op.
+ */
+ @Override
+ public void reset() {
+ // NOOP
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Multi}
+ */
+ public static class MultiSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Multi {
+
+ /**
+ * The default selection size limit.
+ *
+ * @see #setSelectionLimit(int)
+ */
+ public static final int DEFAULT_MAX_SELECTIONS = 1000;
+
+ private int selectionLimit = DEFAULT_MAX_SELECTIONS;
+
+ @Override
+ public boolean select(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // select will fire the event
+ return select(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * All items might not be selected if the limit set using
+ * {@link #setSelectionLimit(int)} is exceeded.
+ */
+ @Override
+ public boolean select(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ // Sanity check
+ checkItemIdsExist(itemIds);
+
+ final boolean selectionWillChange = !selection.containsAll(itemIds)
+ && selection.size() < selectionLimit;
+ if (selectionWillChange) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ selection);
+ if (selection.size() + itemIds.size() >= selectionLimit) {
+ // Add one at a time if there's a risk of overflow
+ Iterator<?> iterator = itemIds.iterator();
+ while (iterator.hasNext()
+ && selection.size() < selectionLimit) {
+ selection.add(iterator.next());
+ }
+ } else {
+ selection.addAll(itemIds);
+ }
+ fireSelectionEvent(oldSelection, selection);
+ }
+ return selectionWillChange;
+ }
+
+ /**
+ * Sets the maximum number of rows that can be selected at once. This is
+ * a mechanism to prevent exhausting server memory in situations where
+ * users select lots of rows. If the limit is reached, newly selected
+ * rows will not become recorded.
+ * <p>
+ * Old selections are not discarded if the current number of selected
+ * row exceeds the new limit.
+ * <p>
+ * The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows.
+ *
+ * @param selectionLimit
+ * the non-negative selection limit to set
+ * @throws IllegalArgumentException
+ * if the limit is negative
+ */
+ public void setSelectionLimit(int selectionLimit) {
+ if (selectionLimit < 0) {
+ throw new IllegalArgumentException(
+ "The selection limit must be non-negative");
+ }
+ this.selectionLimit = selectionLimit;
+ }
+
+ /**
+ * Gets the selection limit.
+ *
+ * @see #setSelectionLimit(int)
+ *
+ * @return the selection limit
+ */
+ public int getSelectionLimit() {
+ return selectionLimit;
+ }
+
+ @Override
+ public boolean deselect(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // deselect will fire the event
+ return deselect(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean deselect(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasCommonElements = !Collections.disjoint(itemIds,
+ selection);
+ if (hasCommonElements) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ selection);
+ selection.removeAll(itemIds);
+ fireSelectionEvent(oldSelection, selection);
+ }
+ return hasCommonElements;
+ }
+
+ @Override
+ public boolean selectAll() {
+ // select will fire the event
+ final Indexed container = grid.getContainerDataSource();
+ if (container != null) {
+ return select(container.getItemIds());
+ } else if (selection.isEmpty()) {
+ return false;
+ } else {
+ /*
+ * this should never happen (no container but has a selection),
+ * but I guess the only theoretically correct course of
+ * action...
+ */
+ return deselectAll();
+ }
+ }
+
+ @Override
+ public boolean deselectAll() {
+ // deselect will fire the event
+ return deselect(getSelectedRows());
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The returned Collection is in <strong>order of selection</strong>
+ * &ndash; the item that was first selected will be first in the
+ * collection, and so on. Should an item have been selected twice
+ * without being deselected in between, it will have remained in its
+ * original position.
+ */
+ @Override
+ public Collection<Object> getSelectedRows() {
+ // overridden only for JavaDoc
+ return super.getSelectedRows();
+ }
+
+ /**
+ * Resets the selection model.
+ * <p>
+ * Equivalent to calling {@link #deselectAll()}
+ */
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ checkItemIdsExist(itemIds);
+
+ boolean changed = false;
+ Set<Object> selectedRows = new HashSet<Object>(itemIds);
+ final Collection<Object> oldSelection = getSelectedRows();
+ SetView<?> added = Sets.difference(selectedRows, selection);
+ if (!added.isEmpty()) {
+ changed = true;
+ selection.addAll(added.immutableCopy());
+ }
+
+ SetView<?> removed = Sets.difference(selection, selectedRows);
+ if (!removed.isEmpty()) {
+ changed = true;
+ selection.removeAll(removed.immutableCopy());
+ }
+
+ if (changed) {
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ return changed;
+ }
+
+ @Override
+ public boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ return setSelected(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a row in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class RowReference implements Serializable {
+ private final Grid grid;
+
+ private Object itemId;
+
+ /**
+ * Creates a new row reference for the given grid.
+ *
+ * @param grid
+ * the grid that the row belongs to
+ */
+ public RowReference(Grid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the identifying information for this row
+ *
+ * @param itemId
+ * the item id of the row
+ */
+ public void set(Object itemId) {
+ this.itemId = itemId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced row.
+ *
+ * @return the grid that contains referenced row
+ */
+ public Grid getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the item id of the row.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Gets the item for the row.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return grid.getContainerDataSource().getItem(itemId);
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a cell in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class CellReference implements Serializable {
+ private final RowReference rowReference;
+
+ private Object propertyId;
+
+ public CellReference(RowReference rowReference) {
+ this.rowReference = rowReference;
+ }
+
+ /**
+ * Sets the identifying information for this cell
+ *
+ * @param propertyId
+ * the property id of the column
+ */
+ public void set(Object propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced cell.
+ *
+ * @return the grid that contains referenced cell
+ */
+ public Grid getGrid() {
+ return rowReference.getGrid();
+ }
+
+ /**
+ * @return the property id of the column
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * @return the property for the cell
+ */
+ public Property<?> getProperty() {
+ return getItem().getItemProperty(propertyId);
+ }
+
+ /**
+ * Gets the item id of the row of the cell.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return rowReference.getItemId();
+ }
+
+ /**
+ * Gets the item for the row of the cell.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return rowReference.getItem();
+ }
+
+ /**
+ * Gets the value of the cell.
+ *
+ * @return the value of the cell
+ */
+ public Object getValue() {
+ return getProperty().getValue();
+ }
+ }
+
+ /**
+ * Callback interface for generating custom style names for data rows
+ *
+ * @see Grid#setRowStyleGenerator(RowStyleGenerator)
+ */
+ public interface RowStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a row
+ *
+ * @param rowReference
+ * The row to generate a style for
+ * @return the style name to add to this row, or {@code null} to not set
+ * any style
+ */
+ public String getStyle(RowReference rowReference);
+ }
+
+ /**
+ * Callback interface for generating custom style names for cells
+ *
+ * @see Grid#setCellStyleGenerator(CellStyleGenerator)
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a column
+ *
+ * @param cellReference
+ * The cell to generate a style for
+ * @return the style name to add to this cell, or {@code null} to not
+ * set any style
+ */
+ public String getStyle(CellReference cellReference);
+ }
+
+ /**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+ protected static abstract class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>>
+ implements Serializable {
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ abstract static class StaticRow<CELLTYPE extends StaticCell> implements
+ Serializable {
+
+ private RowState rowState = new RowState();
+ protected StaticSection<?> section;
+ private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>();
+ private Map<Set<CELLTYPE>, CELLTYPE> cellGroups = new HashMap<Set<CELLTYPE>, CELLTYPE>();
+
+ protected StaticRow(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ protected void addCell(Object propertyId) {
+ CELLTYPE cell = createCell();
+ cell.setColumnId(section.grid.getColumn(propertyId).getState().id);
+ cells.put(propertyId, cell);
+ rowState.cells.add(cell.getCellState());
+ }
+
+ protected void removeCell(Object propertyId) {
+ CELLTYPE cell = cells.remove(propertyId);
+ if (cell != null) {
+ Set<CELLTYPE> cellGroupForCell = getCellGroupForCell(cell);
+ if (cellGroupForCell != null) {
+ removeCellFromGroup(cell, cellGroupForCell);
+ }
+ rowState.cells.remove(cell.getCellState());
+ }
+ }
+
+ private void removeCellFromGroup(CELLTYPE cell,
+ Set<CELLTYPE> cellGroup) {
+ String columnId = cell.getColumnId();
+ for (Set<String> group : rowState.cellGroups.keySet()) {
+ if (group.contains(columnId)) {
+ if (group.size() > 2) {
+ // Update map key correctly
+ CELLTYPE mergedCell = cellGroups.remove(cellGroup);
+ cellGroup.remove(cell);
+ cellGroups.put(cellGroup, mergedCell);
+
+ group.remove(columnId);
+ } else {
+ rowState.cellGroups.remove(group);
+ cellGroups.remove(cellGroup);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of the cell type.
+ *
+ * @return the created cell
+ */
+ protected abstract CELLTYPE createCell();
+
+ protected RowState getRowState() {
+ return rowState;
+ }
+
+ /**
+ * Returns the cell for the given property id on this row. If the
+ * column is merged returned cell is the cell for the whole group.
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the cell for the given property, merged cell for merged
+ * properties, null if not found
+ */
+ public CELLTYPE getCell(Object propertyId) {
+ CELLTYPE cell = cells.get(propertyId);
+ Set<CELLTYPE> cellGroup = getCellGroupForCell(cell);
+ if (cellGroup != null) {
+ cell = cellGroups.get(cellGroup);
+ }
+ return cell;
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param propertyIds
+ * The property ids of columns to merge
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(Object... propertyIds) {
+ assert propertyIds.length > 1 : "You need to merge at least 2 properties";
+
+ Set<CELLTYPE> cells = new HashSet<CELLTYPE>();
+ for (int i = 0; i < propertyIds.length; ++i) {
+ cells.add(getCell(propertyIds[i]));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ assert cells.length > 1 : "You need to merge at least 2 cells";
+
+ return join(new HashSet<CELLTYPE>(Arrays.asList(cells)));
+ }
+
+ protected CELLTYPE join(Set<CELLTYPE> cells) {
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalArgumentException(
+ "Cell already merged");
+ } else if (!this.cells.containsValue(cell)) {
+ throw new IllegalArgumentException(
+ "Cell does not exist on this row");
+ }
+ }
+
+ // Create new cell data for the group
+ CELLTYPE newCell = createCell();
+
+ Set<String> columnGroup = new HashSet<String>();
+ for (CELLTYPE cell : cells) {
+ columnGroup.add(cell.getColumnId());
+ }
+ rowState.cellGroups.put(columnGroup, newCell.getCellState());
+ cellGroups.put(cells, newCell);
+ return newCell;
+ }
+
+ private Set<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (Set<CELLTYPE> group : cellGroups.keySet()) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the custom style name for this row.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return getRowState().styleName;
+ }
+
+ /**
+ * Sets a custom style name for this row.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ getRowState().styleName = styleName;
+ }
+
+ }
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ */
+ abstract static class StaticCell implements Serializable {
+
+ private CellState cellState = new CellState();
+ private StaticRow<?> row;
+
+ protected StaticCell(StaticRow<?> row) {
+ this.row = row;
+ }
+
+ void setColumnId(String id) {
+ cellState.columnId = id;
+ }
+
+ String getColumnId() {
+ return cellState.columnId;
+ }
+
+ /**
+ * Gets the row where this cell is.
+ *
+ * @return row for this cell
+ */
+ public StaticRow<?> getRow() {
+ return row;
+ }
+
+ protected CellState getCellState() {
+ return cellState;
+ }
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ removeComponentIfPresent();
+ cellState.text = text;
+ cellState.type = GridStaticCellType.TEXT;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (cellState.type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type "
+ + cellState.type);
+ }
+ return cellState.text;
+ }
+
+ /**
+ * Returns the HTML content displayed in this cell.
+ *
+ * @return the html
+ *
+ */
+ public String getHtml() {
+ if (cellState.type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type "
+ + cellState.type);
+ }
+ return cellState.html;
+ }
+
+ /**
+ * Sets the HTML content displayed in this cell.
+ *
+ * @param html
+ * the html to set
+ */
+ public void setHtml(String html) {
+ removeComponentIfPresent();
+ cellState.html = html;
+ cellState.type = GridStaticCellType.HTML;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the component displayed in this cell.
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ if (cellState.type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Component from a cell with type "
+ + cellState.type);
+ }
+ return (Component) cellState.connector;
+ }
+
+ /**
+ * Sets the component displayed in this cell.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setComponent(Component component) {
+ removeComponentIfPresent();
+ component.setParent(row.section.grid);
+ cellState.connector = component;
+ cellState.type = GridStaticCellType.WIDGET;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the custom style name for this cell.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return cellState.styleName;
+ }
+
+ /**
+ * Sets a custom style name for this cell.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ cellState.styleName = styleName;
+ row.section.markAsDirty();
+ }
+
+ private void removeComponentIfPresent() {
+ Component component = (Component) cellState.connector;
+ if (component != null) {
+ component.setParent(null);
+ cellState.connector = null;
+ }
+ }
+ }
+
+ protected Grid grid;
+ protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ if (getSectionState().visible != visible) {
+ getSectionState().visible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return getSectionState().visible;
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeRow(StaticRow)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public ROWTYPE removeRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException("No row at given index "
+ + rowIndex);
+ }
+ ROWTYPE row = rows.remove(rowIndex);
+ getSectionState().rows.remove(rowIndex);
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeRow(int)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Gets row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return row at given index
+ */
+ public ROWTYPE getRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException("No row at given index "
+ + rowIndex);
+ }
+ return rows.get(rowIndex);
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ * @see #appendRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ * @see #prependRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE addRowAt(int index) {
+ if (index > rows.size() || index < 0) {
+ throw new IllegalArgumentException(
+ "Unable to add row at index " + index);
+ }
+ ROWTYPE row = createRow();
+ rows.add(index, row);
+ getSectionState().rows.add(index, row.getRowState());
+
+ for (Object id : grid.columns.keySet()) {
+ row.addCell(id);
+ }
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Gets the amount of rows in this section.
+ *
+ * @return row count
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected abstract GridStaticSectionState getSectionState();
+
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that state has changed and it should be redrawn.
+ */
+ protected void markAsDirty() {
+ grid.markAsDirty();
+ }
+
+ /**
+ * Removes a column for given property id from the section.
+ *
+ * @param propertyId
+ * property to be removed
+ */
+ protected void removeColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(propertyId);
+ }
+ }
+
+ /**
+ * Adds a column for given property id to the section.
+ *
+ * @param propertyId
+ * property to be added
+ */
+ protected void addColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.addCell(propertyId);
+ }
+ }
+
+ /**
+ * Performs a sanity check that section is in correct state.
+ *
+ * @throws IllegalStateException
+ * if merged cells are not i n continuous range
+ */
+ protected void sanityCheck() throws IllegalStateException {
+ List<String> columnOrder = grid.getState().columnOrder;
+ for (ROWTYPE row : rows) {
+ for (Set<String> cellGroup : row.getRowState().cellGroups
+ .keySet()) {
+ if (!checkCellGroupAndOrder(columnOrder, cellGroup)) {
+ throw new IllegalStateException(
+ "Not all merged cells were in a continuous range.");
+ }
+ }
+ }
+ }
+
+ private boolean checkCellGroupAndOrder(List<String> columnOrder,
+ Set<String> cellGroup) {
+ if (!columnOrder.containsAll(cellGroup)) {
+ return false;
+ }
+
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!cellGroup.contains(columnOrder.get(i))) {
+ continue;
+ }
+
+ for (int j = 1; j < cellGroup.size(); ++j) {
+ if (!cellGroup.contains(columnOrder.get(i + j))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Represents the header section of a Grid.
+ */
+ protected static class Header extends StaticSection<HeaderRow> {
+
+ private HeaderRow defaultRow = null;
+ private final GridStaticSectionState headerState = new GridStaticSectionState();
+
+ protected Header(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).header = headerState;
+ HeaderRow row = createRow();
+ rows.add(row);
+ setDefaultRow(row);
+ getSectionState().rows.add(row.getRowState());
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special
+ * header row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+
+ if (row != null && !rows.contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the section");
+ }
+
+ if (defaultRow != null) {
+ defaultRow.setDefaultRow(false);
+ }
+
+ if (row != null) {
+ row.setDefaultRow(true);
+ }
+
+ defaultRow = row;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return headerState;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow(this);
+ }
+
+ @Override
+ public HeaderRow removeRow(int rowIndex) {
+ HeaderRow row = super.removeRow(rowIndex);
+ if (row == defaultRow) {
+ // Default Header Row was just removed.
+ setDefaultRow(null);
+ }
+ return row;
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+
+ boolean hasDefaultRow = false;
+ for (HeaderRow row : rows) {
+ if (row.getRowState().defaultRow) {
+ if (!hasDefaultRow) {
+ hasDefaultRow = true;
+ } else {
+ throw new IllegalStateException(
+ "Multiple default rows in header");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a header row in Grid.
+ */
+ public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
+
+ protected HeaderRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ private void setDefaultRow(boolean value) {
+ getRowState().defaultRow = value;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell(this);
+ }
+ }
+
+ /**
+ * Represents a header cell in Grid. Can be a merged cell for multiple
+ * columns.
+ */
+ public static class HeaderCell extends StaticSection.StaticCell {
+
+ protected HeaderCell(HeaderRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * Represents the footer section of a Grid. By default Footer is not
+ * visible.
+ */
+ protected static class Footer extends StaticSection<FooterRow> {
+
+ private final GridStaticSectionState footerState = new GridStaticSectionState();
+
+ protected Footer(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).footer = footerState;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return footerState;
+ }
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow(this);
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+ }
+ }
+
+ /**
+ * Represents a footer row in Grid.
+ */
+ public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
+
+ protected FooterRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell(this);
+ }
+
+ }
+
+ /**
+ * Represents a footer cell in Grid.
+ */
+ public static class FooterCell extends StaticSection.StaticCell {
+
+ protected FooterCell(FooterRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ */
+ public static class Column implements Serializable {
+
+ /**
+ * The state of the column shared to the client
+ */
+ private final GridColumnState state;
+
+ /**
+ * The grid this column is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * Backing property for column
+ */
+ private final Object propertyId;
+
+ private Converter<?, Object> converter;
+
+ /**
+ * A check for allowing the {@link #Column(Grid, GridColumnState)
+ * constructor} to call {@link #setConverter(Converter)} with a
+ * <code>null</code>, even if model and renderer aren't compatible.
+ */
+ private boolean isFirstConverterAssignment = true;
+
+ /**
+ * Internally used constructor.
+ *
+ * @param grid
+ * The grid this column belongs to. Should not be null.
+ * @param state
+ * the shared state of this column
+ * @param propertyId
+ * the backing property id for this column
+ */
+ Column(Grid grid, GridColumnState state, Object propertyId) {
+ this.grid = grid;
+ this.state = state;
+ this.propertyId = propertyId;
+
+ internalSetRenderer(new ObjectRenderer() {
+ private boolean warned = false;
+ private final String DEFAULT_RENDERER_WARNING = "This column uses "
+ + "a dummy default ObjectRenderer. A more suitable "
+ + "renderer should be set using the setRenderer() "
+ + "method.";
+
+ @Override
+ public JsonValue encode(Object value) {
+ if (!warned && !(value instanceof String)) {
+ getLogger().warning(
+ Column.this.toString() + ": "
+ + DEFAULT_RENDERER_WARNING);
+ warned = true;
+ }
+ return super.encode(value);
+ }
+ });
+ }
+
+ /**
+ * Returns the serializable state of this column that is sent to the
+ * client side connector.
+ *
+ * @return the internal state of the column
+ */
+ GridColumnState getState() {
+ return state;
+ }
+
+ /**
+ * Return the property id for the backing property of this Column
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the caption of the header. By default the header caption is
+ * the property id of the column.
+ *
+ * @return the text in the default row of header, null if no default row
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ public String getHeaderCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ return row.getCell(grid.getPropertyIdByColumnId(state.id))
+ .getText();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the caption of the header.
+ *
+ * @param caption
+ * the text to show in the caption
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHeaderCaption(String caption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ row.getCell(grid.getPropertyIdByColumnId(state.id)).setText(
+ caption);
+ }
+ return this;
+ }
+
+ /**
+ * Returns the width (in pixels). By default a column is 100px wide.
+ *
+ * @return the width in pixels of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public double getWidth() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.width;
+ }
+
+ /**
+ * Sets the width (in pixels).
+ * <p>
+ * This overrides any configuration set by any of
+ * {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or
+ * {@link #setMaximumWidth(double)}.
+ *
+ * @param pixelWidth
+ * the new pixel width of the column
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @throws IllegalArgumentException
+ * thrown if pixel width is less than zero
+ */
+ public Column setWidth(double pixelWidth) throws IllegalStateException,
+ IllegalArgumentException {
+ checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0 (in "
+ + toString() + ")");
+ }
+ state.width = pixelWidth;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Marks the column width as undefined meaning that the grid is free to
+ * resize the column based on the cell contents and available space in
+ * the grid.
+ *
+ * @return the column itself
+ */
+ public Column setWidthUndefined() {
+ checkColumnIsAttached();
+ state.width = -1;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Checks if column is attached and throws an
+ * {@link IllegalStateException} if it is not
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkColumnIsAttached() throws IllegalStateException {
+ if (grid.getColumnByColumnId(state.id) == null) {
+ throw new IllegalStateException("Column no longer exists.");
+ }
+ }
+
+ /**
+ * Sets this column as the last frozen column in its grid.
+ *
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the column is no longer attached to any grid
+ * @see Grid#setFrozenColumnCount(int)
+ */
+ public Column setLastFrozenColumn() {
+ checkColumnIsAttached();
+ grid.setFrozenColumnCount(grid.getState(false).columnOrder
+ .indexOf(this) + 1);
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column.
+ * <p>
+ * If a suitable converter isn't defined explicitly, the session
+ * converter factory is used to find a compatible converter.
+ *
+ * @param renderer
+ * the renderer to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if no compatible converter could be found
+ *
+ * @see VaadinSession#getConverterFactory()
+ * @see ConverterUtil#getConverter(Class, Class, VaadinSession)
+ * @see #setConverter(Converter)
+ */
+ public Column setRenderer(Renderer<?> renderer) {
+ boolean success = internalSetRenderer(renderer);
+ if (!success) {
+ throw new IllegalArgumentException("Could not find a "
+ + "converter for converting from the model type "
+ + getModelType() + " to the renderer presentation "
+ + "type " + renderer.getPresentationType() + " (in "
+ + toString() + ")");
+ }
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column and the converter used to convert
+ * from the property value type to the renderer presentation type.
+ *
+ * @param renderer
+ * the renderer to use, cannot be null
+ * @param converter
+ * the converter to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the renderer is already associated with a grid column
+ */
+ public <T> Column setRenderer(Renderer<T> renderer,
+ Converter<? extends T, ?> converter) {
+ if (renderer.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot set a renderer that is already connected to a grid column (in "
+ + toString() + ")");
+ }
+
+ if (getRenderer() != null) {
+ grid.removeExtension(getRenderer());
+ }
+
+ grid.addRenderer(renderer);
+ state.rendererConnector = renderer;
+ setConverter(converter);
+ return this;
+ }
+
+ /**
+ * Sets the converter used to convert from the property value type to
+ * the renderer presentation type.
+ *
+ * @param converter
+ * the converter to use, or {@code null} to not use any
+ * converters
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the types are not compatible
+ */
+ public Column setConverter(Converter<?, ?> converter)
+ throws IllegalArgumentException {
+ Class<?> modelType = getModelType();
+ if (converter != null) {
+ if (!converter.getModelType().isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException("The converter model "
+ + "type " + converter.getModelType() + " is not "
+ + "compatible with the property type " + modelType
+ + " (in " + toString() + ")");
+
+ } else if (!getRenderer().getPresentationType()
+ .isAssignableFrom(converter.getPresentationType())) {
+ throw new IllegalArgumentException("The converter "
+ + "presentation type "
+ + converter.getPresentationType() + " is not "
+ + "compatible with the renderer presentation "
+ + "type " + getRenderer().getPresentationType()
+ + " (in " + toString() + ")");
+ }
+ }
+
+ else {
+ /*
+ * Since the converter is null (i.e. will be removed), we need
+ * to know that the renderer and model are compatible. If not,
+ * we can't allow for this to happen.
+ *
+ * The constructor is allowed to call this method with null
+ * without any compatibility checks, therefore we have a special
+ * case for it.
+ */
+
+ Class<?> rendererPresentationType = getRenderer()
+ .getPresentationType();
+ if (!isFirstConverterAssignment
+ && !rendererPresentationType
+ .isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "Cannot remove converter, "
+ + "as renderer's presentation type "
+ + rendererPresentationType.getName()
+ + " and column's "
+ + "model "
+ + modelType.getName()
+ + " type aren't "
+ + "directly compatible with each other (in "
+ + toString() + ")");
+ }
+ }
+
+ isFirstConverterAssignment = false;
+
+ @SuppressWarnings("unchecked")
+ Converter<?, Object> castConverter = (Converter<?, Object>) converter;
+ this.converter = castConverter;
+
+ return this;
+ }
+
+ /**
+ * Returns the renderer instance used by this column.
+ *
+ * @return the renderer
+ */
+ public Renderer<?> getRenderer() {
+ return (Renderer<?>) getState().rendererConnector;
+ }
+
+ /**
+ * Returns the converter instance used by this column.
+ *
+ * @return the converter
+ */
+ public Converter<?, ?> getConverter() {
+ return converter;
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> boolean internalSetRenderer(Renderer<T> renderer) {
+
+ Converter<? extends T, ?> converter;
+ if (isCompatibleWithProperty(renderer, getConverter())) {
+ // Use the existing converter (possibly none) if types
+ // compatible
+ converter = (Converter<? extends T, ?>) getConverter();
+ } else {
+ converter = ConverterUtil.getConverter(
+ renderer.getPresentationType(), getModelType(),
+ getSession());
+ }
+ setRenderer(renderer, converter);
+ return isCompatibleWithProperty(renderer, converter);
+ }
+
+ private VaadinSession getSession() {
+ UI ui = grid.getUI();
+ return ui != null ? ui.getSession() : null;
+ }
+
+ private boolean isCompatibleWithProperty(Renderer<?> renderer,
+ Converter<?, ?> converter) {
+ Class<?> type;
+ if (converter == null) {
+ type = getModelType();
+ } else {
+ type = converter.getPresentationType();
+ }
+ return renderer.getPresentationType().isAssignableFrom(type);
+ }
+
+ private Class<?> getModelType() {
+ return grid.getContainerDataSource().getType(
+ grid.getPropertyIdByColumnId(state.id));
+ }
+
+ /**
+ * Should sorting controls be available for the column
+ *
+ * @param sortable
+ * <code>true</code> if the sorting controls should be
+ * visible.
+ * @return the column itself
+ */
+ public Column setSortable(boolean sortable) {
+ checkColumnIsAttached();
+
+ if (sortable) {
+ if (!(grid.datasource instanceof Sortable)) {
+ throw new IllegalStateException(
+ "Can't set column "
+ + toString()
+ + " sortable. The Container of Grid does not implement Sortable");
+ } else if (!((Sortable) grid.datasource)
+ .getSortableContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalStateException(
+ "Can't set column "
+ + toString()
+ + " sortable. Container doesn't support sorting by property "
+ + propertyId);
+ }
+ }
+
+ state.sortable = sortable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Are the sorting controls visible in the column header
+ */
+ public boolean isSortable() {
+ return state.sortable;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[propertyId:"
+ + grid.getPropertyIdByColumnId(state.id) + "]";
+ }
+
+ /**
+ * Sets the ratio with which the column expands.
+ * <p>
+ * By default, all columns expand equally (treated as if all of them had
+ * an expand ratio of 1). Once at least one column gets a defined expand
+ * ratio, the implicit expand ratio is removed, and only the defined
+ * expand ratios are taken into account.
+ * <p>
+ * If a column has a defined width ({@link #setWidth(double)}), it
+ * overrides this method's effects.
+ * <p>
+ * <em>Example:</em> A grid with three columns, with expand ratios 0, 1
+ * and 2, respectively. The column with a <strong>ratio of 0 is exactly
+ * as wide as its contents requires</strong>. The column with a ratio of
+ * 1 is as wide as it needs, <strong>plus a third of any excess
+ * space</strong>, bceause we have 3 parts total, and this column
+ * reservs only one of those. The column with a ratio of 2, is as wide
+ * as it needs to be, <strong>plus two thirds</strong> of the excess
+ * width.
+ *
+ * @param expandRatio
+ * the expand ratio of this column. {@code 0} to not have it
+ * expand at all. A negative number to clear the expand
+ * value.
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setWidth(double)
+ */
+ public Column setExpandRatio(int expandRatio)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ getState().expandRatio = expandRatio;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Gets the column's expand ratio.
+ *
+ * @return the column's expand ratio
+ * @see #setExpandRatio(int)
+ */
+ public int getExpandRatio() {
+ return getState().expandRatio;
+ }
+
+ /**
+ * Clears the expand ratio for this column.
+ * <p>
+ * Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)}
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column clearExpandRatio() throws IllegalStateException {
+ return setExpandRatio(-1);
+ }
+
+ /**
+ * Sets the minimum width for this column.
+ * <p>
+ * This defines the minimum guaranteed pixel width of the column
+ * <em>when it is set to expand</em>.
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMinimumWidth(double pixels)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ final double maxwidth = getMaximumWidth();
+ if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
+ throw new IllegalArgumentException("New minimum width ("
+ + pixels + ") was greater than maximum width ("
+ + maxwidth + ")");
+ }
+ getState().minWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Gets the minimum width for this column.
+ *
+ * @return the minimum width for this column
+ * @see #setMinimumWidth(double)
+ */
+ public double getMinimumWidth() {
+ return getState().minWidth;
+ }
+
+ /**
+ * Sets the maximum width for this column.
+ * <p>
+ * This defines the maximum allowed pixel width of the column
+ * <em>when it is set to expand</em>.
+ *
+ * @param pixels
+ * the maximum width
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMaximumWidth(double pixels) {
+ checkColumnIsAttached();
+
+ final double minwidth = getMinimumWidth();
+ if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
+ throw new IllegalArgumentException("New maximum width ("
+ + pixels + ") was less than minimum width (" + minwidth
+ + ")");
+ }
+
+ getState().maxWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Gets the maximum width for this column.
+ *
+ * @return the maximum width for this column
+ * @see #setMaximumWidth(double)
+ */
+ public double getMaximumWidth() {
+ return getState().maxWidth;
+ }
+ }
+
+ /**
+ * An abstract base class for server-side Grid renderers.
+ * {@link com.vaadin.client.widget.grid.Renderer Grid renderers}. This class
+ * currently extends the AbstractExtension superclass, but this fact should
+ * be regarded as an implementation detail and subject to change in a future
+ * major or minor Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ */
+ public static abstract class AbstractRenderer<T> extends AbstractExtension
+ implements Renderer<T> {
+
+ private final Class<T> presentationType;
+
+ protected AbstractRenderer(Class<T> presentationType) {
+ this.presentationType = presentationType;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected Class<Grid> getSupportedParentType() {
+ return Grid.class;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ }
+
+ @Override
+ public Class<T> getPresentationType() {
+ return presentationType;
+ }
+
+ @Override
+ public JsonValue encode(T value) {
+ return encode(value, getPresentationType());
+ }
+
+ /**
+ * Encodes the given value to JSON.
+ * <p>
+ * This is a helper method that can be invoked by an
+ * {@link #encode(Object) encode(T)} override if serializing a value of
+ * type other than {@link #getPresentationType() the presentation type}
+ * is desired. For instance, a {@code Renderer<Date>} could first turn a
+ * date value into a formatted string and return
+ * {@code encode(dateString, String.class)}.
+ *
+ * @param value
+ * the value to be encoded
+ * @param type
+ * the type of the value
+ * @return a JSON representation of the given value
+ */
+ protected <U> JsonValue encode(U value, Class<U> type) {
+ return JsonCodec.encode(value, null, type,
+ getUI().getConnectorTracker()).getEncodedValue();
+ }
+
+ /**
+ * Gets the item id for a row key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a
+ * client. This method can be used to get the item id for the row key
+ * that the client has sent.
+ *
+ * @param rowKey
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ */
+ protected Object getItemId(String rowKey) {
+ return getParentGrid().getKeyMapper().getItemId(rowKey);
+ }
+
+ /**
+ * Gets the column for a column id.
+ * <p>
+ * An id is used to identify a particular column on both a server and a
+ * client. This method can be used to get the column for the column id
+ * that the client has sent.
+ *
+ * @param columnId
+ * the column id for which to retrieve a column
+ * @return the column corresponding to {@code columnId}
+ */
+ protected Column getColumn(String columnId) {
+ return getParentGrid().getColumnByColumnId(columnId);
+ }
+
+ /**
+ * Gets the parent Grid of the renderer.
+ *
+ * @return parent grid
+ * @throws IllegalStateException
+ * if parent is not Grid
+ */
+ protected Grid getParentGrid() {
+ if (getParent() instanceof Grid) {
+ Grid grid = (Grid) getParent();
+ return grid;
+ } else {
+ throw new IllegalStateException(
+ "Renderers can be used only with Grid");
+ }
+ }
+ }
+
+ /**
+ * The data source attached to the grid
+ */
+ private Container.Indexed datasource;
+
+ /**
+ * Property id to column instance mapping
+ */
+ private final Map<Object, Column> columns = new HashMap<Object, Column>();
+
+ /**
+ * Key generator for column server-to-client communication
+ */
+ private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();
+
+ /**
+ * The current sort order
+ */
+ private final List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ /**
+ * Property listener for listening to changes in data source properties.
+ */
+ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ Collection<?> properties = new HashSet<Object>(event.getContainer()
+ .getContainerPropertyIds());
+
+ // Find columns that need to be removed.
+ List<Column> removedColumns = new LinkedList<Column>();
+ for (Object propertyId : columns.keySet()) {
+ if (!properties.contains(propertyId)) {
+ removedColumns.add(getColumn(propertyId));
+ }
+ }
+
+ // Actually remove columns.
+ for (Column column : removedColumns) {
+ Object propertyId = column.getPropertyId();
+ internalRemoveColumn(propertyId);
+ columnKeys.remove(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removedColumns);
+
+ // Add new columns
+ List<Column> addedColumns = new LinkedList<Column>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ addedColumns.add(appendColumn(propertyId));
+ }
+ }
+ datasourceExtension.columnsAdded(addedColumns);
+
+ if (getFrozenColumnCount() > columns.size()) {
+ setFrozenColumnCount(columns.size());
+ }
+
+ // Update sortable columns
+ if (event.getContainer() instanceof Sortable) {
+ Collection<?> sortableProperties = ((Sortable) event
+ .getContainer()).getSortableContainerPropertyIds();
+ for (Entry<Object, Column> columnEntry : columns.entrySet()) {
+ columnEntry.getValue().setSortable(
+ sortableProperties.contains(columnEntry.getKey()));
+ }
+ }
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ /**
+ * The selection model that is currently in use. Never <code>null</code>
+ * after the constructor has been run.
+ */
+ private SelectionModel selectionModel;
+
+ /**
+ * Used to know whether selection change events originate from the server or
+ * the client so the selection change handler knows whether the changes
+ * should be sent to the client.
+ */
+ private boolean applyingSelectionFromClient;
+
+ private final Header header = new Header(this);
+ private final Footer footer = new Footer(this);
+
+ private Object editedItemId = null;
+ private FieldGroup editorFieldGroup = new CustomFieldGroup();
+
+ private CellStyleGenerator cellStyleGenerator;
+ private RowStyleGenerator rowStyleGenerator;
+
+ /**
+ * <code>true</code> if Grid is using the internal IndexedContainer created
+ * in Grid() constructor, or <code>false</code> if the user has set their
+ * own Container.
+ *
+ * @see #setContainerDataSource()
+ * @see #Grid()
+ */
+ private boolean defaultContainer = true;
+
+ private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+ .findMethod(SelectionListener.class, "select", SelectionEvent.class);
+
+ private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
+ .findMethod(SortListener.class, "sort", SortEvent.class);
+
+ /**
+ * Creates a new Grid with a new {@link IndexedContainer} as the data
+ * source.
+ */
+ public Grid() {
+ this(null, null);
+ }
+
+ /**
+ * Creates a new Grid using the given data source.
+ *
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public Grid(final Container.Indexed dataSource) {
+ this(null, dataSource);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and a new
+ * {@link IndexedContainer} data source.
+ *
+ * @param caption
+ * the caption of the grid
+ */
+ public Grid(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and data source. If the data
+ * source is null, a new {@link IndexedContainer} will be used.
+ *
+ * @param caption
+ * the caption of the grid
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public Grid(String caption, Container.Indexed dataSource) {
+ if (dataSource == null) {
+ internalSetContainerDataSource(new IndexedContainer());
+ } else {
+ setContainerDataSource(dataSource);
+ }
+ setCaption(caption);
+ initGrid();
+ }
+
+ /**
+ * Grid initial setup
+ */
+ private void initGrid() {
+ setSelectionMode(SelectionMode.SINGLE);
+ addSelectionListener(new SelectionListener() {
+ @Override
+ public void select(SelectionEvent event) {
+ if (applyingSelectionFromClient) {
+ /*
+ * Avoid sending changes back to the client if they
+ * originated from the client. Instead, the RPC handler is
+ * responsible for keeping track of the resulting selection
+ * state and notifying the client if it doens't match the
+ * expectation.
+ */
+ return;
+ }
+
+ /*
+ * The rows are pinned here to ensure that the client gets the
+ * correct key from server when the selected row is first
+ * loaded.
+ *
+ * Once the client has gotten info that it is supposed to select
+ * a row, it will pin the data from the client side as well and
+ * it will be unpinned once it gets deselected. Nothing on the
+ * server side should ever unpin anything from KeyMapper.
+ * Pinning is mostly a client feature and is only used when
+ * selecting something from the server side.
+ */
+ for (Object addedItemId : event.getAdded()) {
+ if (!getKeyMapper().isPinned(addedItemId)) {
+ getKeyMapper().pin(addedItemId);
+ }
+ }
+
+ getState().selectedKeys = getKeyMapper().getKeys(
+ getSelectedRows());
+ }
+ });
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void select(List<String> selection) {
+ Collection<Object> receivedSelection = getKeyMapper()
+ .getItemIds(selection);
+
+ applyingSelectionFromClient = true;
+ try {
+ SelectionModel selectionModel = getSelectionModel();
+ if (selectionModel instanceof SelectionModel.Single
+ && selection.size() <= 1) {
+ Object select = null;
+ if (selection.size() == 1) {
+ select = getKeyMapper().getItemId(selection.get(0));
+ }
+ ((SelectionModel.Single) selectionModel).select(select);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ ((SelectionModel.Multi) selectionModel)
+ .setSelected(receivedSelection);
+ } else {
+ throw new IllegalStateException("SelectionModel "
+ + selectionModel.getClass().getSimpleName()
+ + " does not support selecting the given "
+ + selection.size() + " items.");
+ }
+ } finally {
+ applyingSelectionFromClient = false;
+ }
+
+ Collection<Object> actualSelection = getSelectedRows();
+
+ // Make sure all selected rows are pinned
+ for (Object itemId : actualSelection) {
+ if (!getKeyMapper().isPinned(itemId)) {
+ getKeyMapper().pin(itemId);
+ }
+ }
+
+ // Don't mark as dirty since this might be the expected state
+ getState(false).selectedKeys = getKeyMapper().getKeys(
+ actualSelection);
+
+ JsonObject diffState = getUI().getConnectorTracker()
+ .getDiffState(Grid.this);
+
+ final String diffstateKey = "selectedKeys";
+
+ assert diffState.hasKey(diffstateKey) : "Field name has changed";
+
+ if (receivedSelection.equals(actualSelection)) {
+ /*
+ * We ended up with the same selection state that the client
+ * sent us. There's nothing to send back to the client, just
+ * update the diffstate so subsequent changes will be
+ * detected.
+ */
+ JsonArray diffSelected = Json.createArray();
+ for (String rowKey : getState(false).selectedKeys) {
+ diffSelected.set(diffSelected.length(), rowKey);
+ }
+ diffState.put(diffstateKey, diffSelected);
+ } else {
+ /*
+ * Actual selection is not what the client expects. Make
+ * sure the client gets a state change event by clearing the
+ * diffstate and marking as dirty
+ */
+ diffState.remove(diffstateKey);
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void sort(String[] columnIds, SortDirection[] directions,
+ boolean userOriginated) {
+ assert columnIds.length == directions.length;
+
+ List<SortOrder> order = new ArrayList<SortOrder>(
+ columnIds.length);
+ for (int i = 0; i < columnIds.length; i++) {
+ Object propertyId = getPropertyIdByColumnId(columnIds[i]);
+ order.add(new SortOrder(propertyId, directions[i]));
+ }
+
+ setSortOrder(order, userOriginated);
+ }
+
+ @Override
+ public void selectAll() {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Not a multi selection model!";
+
+ ((SelectionModel.Multi) getSelectionModel()).selectAll();
+ }
+
+ @Override
+ public void itemClick(String rowKey, String columnId,
+ MouseEventDetails details) {
+ Object itemId = getKeyMapper().getItemId(rowKey);
+ Item item = datasource.getItem(itemId);
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ fireEvent(new ItemClickEvent(Grid.this, item, itemId,
+ propertyId, details));
+ }
+ });
+
+ registerRpc(new EditorServerRpc() {
+
+ @Override
+ public void bind(int rowIndex) {
+ boolean success = false;
+ try {
+ Object id = getContainerDataSource().getIdByIndex(rowIndex);
+ if (editedItemId == null) {
+ editedItemId = id;
+ }
+
+ if (editedItemId.equals(id)) {
+ success = true;
+ doEditItem();
+ }
+ } catch (Exception e) {
+ handleError(e);
+ }
+ getEditorRpc().confirmBind(success);
+ }
+
+ @Override
+ public void cancel(int rowIndex) {
+ try {
+ // For future proofing even though cannot currently fail
+ doCancelEditor();
+ } catch (Exception e) {
+ handleError(e);
+ }
+ }
+
+ @Override
+ public void save(int rowIndex) {
+ boolean success = false;
+ try {
+ saveEditor();
+ success = true;
+ } catch (Exception e) {
+ handleError(e);
+ }
+ getEditorRpc().confirmSave(success);
+ }
+
+ private void handleError(Exception e) {
+ com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).error(
+ new ConnectorErrorEvent(Grid.this, e));
+ }
+ });
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ try {
+ header.sanityCheck();
+ footer.sanityCheck();
+ } catch (Exception e) {
+ e.printStackTrace();
+ setComponentError(new ErrorMessage() {
+
+ @Override
+ public ErrorLevel getErrorLevel() {
+ return ErrorLevel.CRITICAL;
+ }
+
+ @Override
+ public String getFormattedHtmlMessage() {
+ return "Incorrectly merged cells";
+ }
+
+ });
+ }
+
+ super.beforeClientResponse(initial);
+ }
+
+ /**
+ * Sets the grid data source.
+ *
+ * @param container
+ * The container data source. Cannot be null.
+ * @throws IllegalArgumentException
+ * if the data source is null
+ */
+ public void setContainerDataSource(Container.Indexed container) {
+ defaultContainer = false;
+ internalSetContainerDataSource(container);
+ }
+
+ private void internalSetContainerDataSource(Container.Indexed container) {
+ if (container == null) {
+ throw new IllegalArgumentException(
+ "Cannot set the datasource to null");
+ }
+ if (datasource == container) {
+ return;
+ }
+
+ // Remove old listeners
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .removePropertySetChangeListener(propertyListener);
+ }
+
+ if (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ datasource = container;
+
+ resetEditor();
+
+ //
+ // Adjust sort order
+ //
+
+ if (container instanceof Container.Sortable) {
+
+ // If the container is sortable, go through the current sort order
+ // and match each item to the sortable properties of the new
+ // container. If the new container does not support an item in the
+ // current sort order, that item is removed from the current sort
+ // order list.
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ Iterator<SortOrder> i = sortOrder.iterator();
+ while (i.hasNext()) {
+ if (!sortableProps.contains(i.next().getPropertyId())) {
+ i.remove();
+ }
+ }
+
+ sort(false);
+ } else {
+ // Clear sorting order. Don't sort.
+ sortOrder.clear();
+ }
+
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this, columnKeys);
+
+ /*
+ * selectionModel == null when the invocation comes from the
+ * constructor.
+ */
+ if (selectionModel != null) {
+ selectionModel.reset();
+ }
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+ /*
+ * activeRowHandler will be updated by the client-side request that
+ * occurs on container change - no need to actively re-insert any
+ * ValueChangeListeners at this point.
+ */
+
+ setFrozenColumnCount(0);
+
+ if (columns.isEmpty()) {
+ // Add columns
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ Column column = appendColumn(propertyId);
+
+ // Initial sorting is defined by container
+ if (datasource instanceof Sortable) {
+ column.setSortable(((Sortable) datasource)
+ .getSortableContainerPropertyIds().contains(
+ propertyId));
+ } else {
+ column.setSortable(false);
+ }
+ }
+ } else {
+ Collection<?> properties = datasource.getContainerPropertyIds();
+ for (Object property : columns.keySet()) {
+ if (!properties.contains(property)) {
+ throw new IllegalStateException(
+ "Found at least one column in Grid that does not exist in the given container: "
+ + property
+ + " with the header \""
+ + getColumn(property).getHeaderCaption()
+ + "\"");
+ }
+
+ if (!(datasource instanceof Sortable)
+ || !((Sortable) datasource)
+ .getSortableContainerPropertyIds().contains(
+ property)) {
+ columns.get(property).setSortable(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the grid data source.
+ *
+ * @return the container data source of the grid
+ */
+ public Container.Indexed getContainerDataSource() {
+ return datasource;
+ }
+
+ /**
+ * Returns a column based on the property id
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the column or <code>null</code> if not found
+ */
+ public Column getColumn(Object propertyId) {
+ return columns.get(propertyId);
+ }
+
+ /**
+ * Returns a copy of currently configures columns in their current visual
+ * order in this Grid.
+ *
+ * @return unmodifiable copy of current columns in visual order
+ */
+ public List<Column> getColumns() {
+ List<Column> columns = new ArrayList<Grid.Column>();
+ for (String columnId : getState(false).columnOrder) {
+ columns.add(getColumnByColumnId(columnId));
+ }
+ return Collections.unmodifiableList(columns);
+ }
+
+ /**
+ * Adds a new Column to Grid. Also adds the property to container with data
+ * type String, if property for column does not exist in it. Default value
+ * for the new property is an empty String.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid
+ */
+
+ public Column addColumn(Object propertyId) throws IllegalStateException {
+ if (datasource.getContainerPropertyIds().contains(propertyId)
+ && !columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ } else {
+ addColumnProperty(propertyId, String.class, "");
+ }
+
+ // Inform the data provider of this new column.
+ Column column = getColumn(propertyId);
+ List<Column> addedColumns = new ArrayList<Column>();
+ addedColumns.add(column);
+ datasourceExtension.columnsAdded(addedColumns);
+
+ return column;
+ }
+
+ /**
+ * Adds a new Column to Grid. This function makes sure that the property
+ * with the given id and data type exists in the container. If property does
+ * not exists, it will be created.
+ * <p>
+ * Default value for the new property is 0 if type is Integer, Double and
+ * Float. If type is String, default value is an empty string. For all other
+ * types the default value is null.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @param type
+ * the data type for the new property
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid or
+ * property already exists in the container with wrong type
+ */
+ public Column addColumn(Object propertyId, Class<?> type) {
+ addColumnProperty(propertyId, type, null);
+ return getColumn(propertyId);
+ }
+
+ protected void addColumnProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws IllegalStateException {
+ if (!defaultContainer) {
+ throw new IllegalStateException(
+ "Container for this Grid is not a default container from Grid() constructor");
+ }
+
+ if (!columns.containsKey(propertyId)) {
+ if (!datasource.getContainerPropertyIds().contains(propertyId)) {
+ datasource.addContainerProperty(propertyId, type, defaultValue);
+ } else {
+ Property<?> containerProperty = datasource
+ .getContainerProperty(datasource.firstItemId(),
+ propertyId);
+ if (containerProperty.getType() == type) {
+ appendColumn(propertyId);
+ } else {
+ throw new IllegalStateException(
+ "DataSource already has the given property "
+ + propertyId + " with a different type");
+ }
+ }
+ } else {
+ throw new IllegalStateException(
+ "Grid already has a column for property " + propertyId);
+ }
+ }
+
+ /**
+ * Removes all columns from this Grid.
+ */
+ public void removeAllColumns() {
+ List<Column> removed = new ArrayList<Column>(columns.values());
+ Set<Object> properties = new HashSet<Object>(columns.keySet());
+ for (Object propertyId : properties) {
+ removeColumn(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a {@link Column} by
+ * referencing its generated state id. Also used by {@link Column} to verify
+ * if it has been detached from the {@link Grid}.
+ *
+ * @param columnId
+ * the client id generated for the column when the column is
+ * added to the grid
+ * @return the column with the id or <code>null</code> if not found
+ */
+ Column getColumnByColumnId(String columnId) {
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ return getColumn(propertyId);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a property id by referencing
+ * the columns generated state id.
+ *
+ * @param columnId
+ * The state id of the column
+ * @return The column instance or null if not found
+ */
+ Object getPropertyIdByColumnId(String columnId) {
+ return columnKeys.get(columnId);
+ }
+
+ @Override
+ protected GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected GridState getState(boolean markAsDirty) {
+ return (GridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Creates a new column based on a property id and appends it as the last
+ * column.
+ *
+ * @param datasourcePropertyId
+ * The property id of a property in the datasource
+ */
+ private Column appendColumn(Object datasourcePropertyId) {
+ if (datasourcePropertyId == null) {
+ throw new IllegalArgumentException("Property id cannot be null");
+ }
+ assert datasource.getContainerPropertyIds().contains(
+ datasourcePropertyId) : "Datasource should contain the property id";
+
+ GridColumnState columnState = new GridColumnState();
+ columnState.id = columnKeys.key(datasourcePropertyId);
+
+ Column column = new Column(this, columnState, datasourcePropertyId);
+ columns.put(datasourcePropertyId, column);
+
+ getState().columns.add(columnState);
+ getState().columnOrder.add(columnState.id);
+ header.addColumn(datasourcePropertyId);
+ footer.addColumn(datasourcePropertyId);
+
+ column.setHeaderCaption(SharedUtil.propertyIdToHumanFriendly(String
+ .valueOf(datasourcePropertyId)));
+
+ return column;
+ }
+
+ /**
+ * Removes a column from Grid based on a property id.
+ *
+ * @param propertyId
+ * The property id of column to be removed
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for given property id in this grid
+ */
+ public void removeColumn(Object propertyId) throws IllegalArgumentException {
+ if (!columns.keySet().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "There is no column for given property id " + propertyId);
+ }
+
+ List<Column> removed = new ArrayList<Column>();
+ removed.add(getColumn(propertyId));
+ internalRemoveColumn(propertyId);
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ private void internalRemoveColumn(Object propertyId) {
+ setEditorField(propertyId, null);
+ header.removeColumn(propertyId);
+ footer.removeColumn(propertyId);
+ Column column = columns.remove(propertyId);
+ getState().columnOrder.remove(columnKeys.key(propertyId));
+ getState().columns.remove(column.getState());
+ removeExtension(column.getRenderer());
+ }
+
+ /**
+ * Sets a new column order for the grid. All columns which are not ordered
+ * here will remain in the order they were before as the last columns of
+ * grid.
+ *
+ * @param propertyIds
+ * properties in the order columns should be
+ */
+ public void setColumnOrder(Object... propertyIds) {
+ List<String> columnOrder = new ArrayList<String>();
+ for (Object propertyId : propertyIds) {
+ if (columns.containsKey(propertyId)) {
+ columnOrder.add(columnKeys.key(propertyId));
+ } else {
+ throw new IllegalArgumentException(
+ "Grid does not contain column for property "
+ + String.valueOf(propertyId));
+ }
+ }
+
+ List<String> stateColumnOrder = getState().columnOrder;
+ if (stateColumnOrder.size() != columnOrder.size()) {
+ stateColumnOrder.removeAll(columnOrder);
+ columnOrder.addAll(stateColumnOrder);
+ }
+ getState().columnOrder = columnOrder;
+ }
+
+ /**
+ * Sets the number of frozen columns in this grid. Setting the count to 0
+ * means that no data columns will be frozen, but the built-in selection
+ * checkbox column will still be frozen if it's in use. Setting the count to
+ * -1 will also disable the selection column.
+ * <p>
+ * The default value is 0.
+ *
+ * @param numberOfColumns
+ * the number of columns that should be frozen
+ *
+ * @throws IllegalArgumentException
+ * if the column count is < 0 or > the number of visible columns
+ */
+ public void setFrozenColumnCount(int numberOfColumns) {
+ if (numberOfColumns < -1 || numberOfColumns > columns.size()) {
+ throw new IllegalArgumentException(
+ "count must be between -1 and the current number of columns ("
+ + columns + ")");
+ }
+
+ getState().frozenColumnCount = numberOfColumns;
+ }
+
+ /**
+ * Gets the number of frozen columns in this grid. 0 means that no data
+ * columns will be frozen, but the built-in selection checkbox column will
+ * still be frozen if it's in use. -1 means that not even the selection
+ * column is frozen.
+ *
+ * @see #setFrozenColumnCount(int)
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount() {
+ return getState(false).frozenColumnCount;
+ }
+
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId) throws IllegalArgumentException {
+ scrollTo(itemId, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId, ScrollDestination destination)
+ throws IllegalArgumentException {
+
+ int row = datasource.indexOfId(itemId);
+
+ if (row == -1) {
+ throw new IllegalArgumentException(
+ "Item with specified ID does not exist in data source");
+ }
+
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToEnd();
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead. If <code>null</code> is given, then Grid's
+ * height is undefined
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInifinite(double)
+ * infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ */
+ public void setHeightByRows(double rows) {
+ if (rows <= 0.0d) {
+ throw new IllegalArgumentException(
+ "More than zero rows must be shown.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "Grid doesn't support infinite heights");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("NaN is not a valid row count");
+ }
+
+ getState().heightByRows = rows;
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ *
+ * @return the amount of rows that are being shown in Grid's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return getState(false).heightByRows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via a {@code setHeight}-method, and behave as a traditional Component.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ getState().heightMode = heightMode;
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return getState(false).heightMode;
+ }
+
+ /* Selection related methods: */
+
+ /**
+ * Takes a new {@link SelectionModel} into use.
+ * <p>
+ * The SelectionModel that is previously in use will have all its items
+ * deselected.
+ * <p>
+ * If the given SelectionModel is already in use, this method does nothing.
+ *
+ * @param selectionModel
+ * the new SelectionModel to use
+ * @throws IllegalArgumentException
+ * if {@code selectionModel} is <code>null</code>
+ */
+ public void setSelectionModel(SelectionModel selectionModel)
+ throws IllegalArgumentException {
+ if (selectionModel == null) {
+ throw new IllegalArgumentException(
+ "Selection model may not be null");
+ }
+
+ if (this.selectionModel != selectionModel) {
+ // this.selectionModel is null on init
+ if (this.selectionModel != null) {
+ this.selectionModel.reset();
+ this.selectionModel.setGrid(null);
+ }
+
+ this.selectionModel = selectionModel;
+ this.selectionModel.setGrid(this);
+ this.selectionModel.reset();
+
+ if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.SINGLE;
+ } else if (selectionModel.getClass().equals(
+ MultiSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.MULTI;
+ } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.NONE;
+ } else {
+ throw new UnsupportedOperationException("Grid currently "
+ + "supports only its own bundled selection models");
+ }
+ }
+ }
+
+ /**
+ * Returns the currently used {@link SelectionModel}.
+ *
+ * @return the currently used SelectionModel
+ */
+ public SelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets the Grid's selection mode.
+ * <p>
+ * Grid supports three selection modes: multiselect, single select and no
+ * selection, and this is a convenience method for choosing between one of
+ * them.
+ * <p>
+ * Technically, this method is a shortcut that can be used instead of
+ * calling {@code setSelectionModel} with a specific SelectionModel
+ * instance. Grid comes with three built-in SelectionModel classes, and the
+ * {@link SelectionMode} enum represents each of them.
+ * <p>
+ * Essentially, the two following method calls are equivalent:
+ * <p>
+ * <code><pre>
+ * grid.setSelectionMode(SelectionMode.MULTI);
+ * grid.setSelectionModel(new MultiSelectionMode());
+ * </pre></code>
+ *
+ *
+ * @param selectionMode
+ * the selection mode to switch to
+ * @return The {@link SelectionModel} instance that was taken into use
+ * @throws IllegalArgumentException
+ * if {@code selectionMode} is <code>null</code>
+ * @see SelectionModel
+ */
+ public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+ throws IllegalArgumentException {
+ if (selectionMode == null) {
+ throw new IllegalArgumentException("selection mode may not be null");
+ }
+ final SelectionModel newSelectionModel = selectionMode.createModel();
+ setSelectionModel(newSelectionModel);
+ return newSelectionModel;
+ }
+
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ // keep this javadoc in sync with SelectionModel.isSelected
+ public boolean isSelected(Object itemId) {
+ return selectionModel.isSelected(itemId);
+ }
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ // keep this javadoc in sync with SelectionModel.getSelectedRows
+ public Collection<Object> getSelectedRows() {
+ return getSelectionModel().getSelectedRows();
+ }
+
+ /**
+ * Gets the item id of the currently selected item.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} is supported.
+ *
+ * @return the item id of the currently selected item, or <code>null</code>
+ * if nothing is selected
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+ public Object getSelectedRow() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).getSelectedRow();
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ throw new IllegalStateException("Cannot get unique selected row: "
+ + "Grid is in multiselect mode "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot get selected row: "
+ + "Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot get selected row: "
+ + "Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as selected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemIds
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {@code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.select
+ public boolean select(Object itemId) throws IllegalArgumentException,
+ IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.deselect
+ public boolean deselect(Object itemId) throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (isSelected(itemId)) {
+ return ((SelectionModel.Single) selectionModel).select(null);
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Fires a selection change event.
+ * <p>
+ * <strong>Note:</strong> This is not a method that should be called by
+ * application logic. This method is publicly accessible only so that
+ * {@link SelectionModel SelectionModels} would be able to inform Grid of
+ * these events.
+ *
+ * @param addedSelections
+ * the selections that were added by this event
+ * @param removedSelections
+ * the selections that were removed by this event
+ */
+ public void fireSelectionEvent(Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ fireEvent(new SelectionEvent(this, oldSelection, newSelection));
+ }
+
+ @Override
+ public void addSelectionListener(SelectionListener listener) {
+ addListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeSelectionListener(SelectionListener listener) {
+ removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ /**
+ * Gets the
+ * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper
+ * DataProviderKeyMapper} being used by the data source.
+ *
+ * @return the key mapper being used by the data source
+ */
+ DataProviderKeyMapper getKeyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
+ * Adds a renderer to this grid's connector hierarchy.
+ *
+ * @param renderer
+ * the renderer to add
+ */
+ void addRenderer(Renderer<?> renderer) {
+ addExtension(renderer);
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param s
+ * a sort instance
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sort this Grid in ascending order by a specified property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId) {
+ sort(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sort this Grid in user-specified {@link SortOrder} by a property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ * @param direction
+ * a sort order value (ascending/descending)
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId, SortDirection direction) {
+ sort(Sort.by(propertyId, direction));
+ }
+
+ /**
+ * Clear the current sort order, and re-sort the grid.
+ */
+ public void clearSortOrder() {
+ sortOrder.clear();
+ sort(false);
+ }
+
+ /**
+ * Sets the sort order to use.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param order
+ * a sort order list.
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if order is null or trying to sort by non-existing property
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, false);
+ }
+
+ private void setSortOrder(List<SortOrder> order, boolean userOriginated)
+ throws IllegalStateException, IllegalArgumentException {
+ if (!(getContainerDataSource() instanceof Container.Sortable)) {
+ throw new IllegalStateException(
+ "Attached container is not sortable (does not implement Container.Sortable)");
+ }
+
+ if (order == null) {
+ throw new IllegalArgumentException("Order list may not be null!");
+ }
+
+ sortOrder.clear();
+
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ for (SortOrder o : order) {
+ if (!sortableProps.contains(o.getPropertyId())) {
+ throw new IllegalArgumentException(
+ "Property "
+ + o.getPropertyId()
+ + " does not exist or is not sortable in the current container");
+ }
+ }
+
+ sortOrder.addAll(order);
+ sort(userOriginated);
+ }
+
+ /**
+ * Get the current sort order list.
+ *
+ * @return a sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(boolean userOriginated) {
+
+ Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ Container.Sortable cs = (Container.Sortable) c;
+
+ final int items = sortOrder.size();
+ Object[] propertyIds = new Object[items];
+ boolean[] directions = new boolean[items];
+
+ SortDirection[] stateDirs = new SortDirection[items];
+
+ for (int i = 0; i < items; ++i) {
+ SortOrder order = sortOrder.get(i);
+
+ stateDirs[i] = order.getDirection();
+ propertyIds[i] = order.getPropertyId();
+ switch (order.getDirection()) {
+ case ASCENDING:
+ directions[i] = true;
+ break;
+ case DESCENDING:
+ directions[i] = false;
+ break;
+ default:
+ throw new IllegalArgumentException("getDirection() of "
+ + order + " returned an unexpected value");
+ }
+ }
+
+ cs.sort(propertyIds, directions);
+
+ fireEvent(new SortEvent(this, new ArrayList<SortOrder>(sortOrder),
+ userOriginated));
+
+ if (columns.keySet().containsAll(Arrays.asList(propertyIds))) {
+ String[] columnKeys = new String[items];
+ for (int i = 0; i < items; ++i) {
+ columnKeys[i] = this.columnKeys.key(propertyIds[i]);
+ }
+ getState().sortColumns = columnKeys;
+ getState(false).sortDirs = stateDirs;
+ } else {
+ // Not all sorted properties are in Grid. Remove any indicators.
+ getState().sortColumns = new String[] {};
+ getState(false).sortDirs = new SortDirection[] {};
+ }
+ } else {
+ throw new IllegalStateException(
+ "Container is not sortable (does not implement Container.Sortable)");
+ }
+ }
+
+ /**
+ * Adds a sort order change listener that gets notified when the sort order
+ * changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ @Override
+ public void addSortListener(SortListener listener) {
+ addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortListener(SortListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ @Override
+ public void removeSortListener(SortListener listener) {
+ removeListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /* Grid Headers */
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ protected Header getHeader() {
+ return header;
+ }
+
+ /**
+ * Gets the header row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return header row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public HeaderRow getHeaderRow(int rowIndex) {
+ return header.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the header section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow addHeaderRowAt(int index) {
+ return header.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the header section.
+ *
+ * @return the new row
+ * @see #prependHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow appendHeaderRow() {
+ return header.appendRow();
+ }
+
+ /**
+ * Returns the current default row of the header section. The default row is
+ * a special header row providing a user interface for sorting columns.
+ * Setting a header text for column updates cells in the default header.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultHeaderRow() {
+ return header.getDefaultRow();
+ }
+
+ /**
+ * Gets the row count for the header section.
+ *
+ * @return row count
+ */
+ public int getHeaderRowCount() {
+ return header.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the header section.
+ *
+ * @return the new row
+ * @see #appendHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow prependHeaderRow() {
+ return header.prependRow();
+ }
+
+ /**
+ * Removes the given row from the header section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeHeaderRow(int)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(HeaderRow row) {
+ header.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the header section.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(int rowIndex) {
+ header.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the default row of the header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * header does not contain the row
+ */
+ public void setDefaultHeaderRow(HeaderRow row) {
+ header.setDefaultRow(row);
+ }
+
+ /**
+ * Sets the visibility of the header section.
+ *
+ * @param visible
+ * true to show header section, false to hide
+ */
+ public void setHeaderVisible(boolean visible) {
+ header.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the header section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isHeaderVisible() {
+ return header.isVisible();
+ }
+
+ /* Grid Footers */
+
+ /**
+ * Returns the footer section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the footer
+ */
+ protected Footer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Gets the footer row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return footer row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public FooterRow getFooterRow(int rowIndex) {
+ return footer.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the footer section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow addFooterRowAt(int index) {
+ return footer.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the footer section.
+ *
+ * @return the new row
+ * @see #prependFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow appendFooterRow() {
+ return footer.appendRow();
+ }
+
+ /**
+ * Gets the row count for the footer.
+ *
+ * @return row count
+ */
+ public int getFooterRowCount() {
+ return footer.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the footer section.
+ *
+ * @return the new row
+ * @see #appendFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow prependFooterRow() {
+ return footer.prependRow();
+ }
+
+ /**
+ * Removes the given row from the footer section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeFooterRow(int)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(FooterRow row) {
+ footer.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the footer section.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeFooterRow(FooterRow)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(int rowIndex) {
+ footer.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the visibility of the footer section.
+ *
+ * @param visible
+ * true to show footer section, false to hide
+ */
+ public void setFooterVisible(boolean visible) {
+ footer.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the footer section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isFooterVisible() {
+ return footer.isVisible();
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ List<Component> componentList = new ArrayList<Component>();
+
+ Header header = getHeader();
+ for (int i = 0; i < header.getRowCount(); ++i) {
+ HeaderRow row = header.getRow(i);
+ for (Object propId : columns.keySet()) {
+ HeaderCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ Footer footer = getFooter();
+ for (int i = 0; i < footer.getRowCount(); ++i) {
+ FooterRow row = footer.getRow(i);
+ for (Object propId : columns.keySet()) {
+ FooterCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ componentList.addAll(getEditorFields());
+ return componentList.iterator();
+ }
+
+ @Override
+ public boolean isRendered(Component childComponent) {
+ if (getEditorFields().contains(childComponent)) {
+ // Only render editor fields if the editor is open
+ return isEditorActive();
+ } else {
+ // TODO Header and footer components should also only be rendered if
+ // the header/footer is visible
+ return true;
+ }
+ }
+
+ EditorClientRpc getEditorRpc() {
+ return getRpcProxy(EditorClientRpc.class);
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for cells
+ *
+ * @param cellStyleGenerator
+ * the cell style generator to set, or <code>null</code> to
+ * remove a previously set generator
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ getState().hasCellStyleGenerator = (cellStyleGenerator != null);
+
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for cells
+ *
+ * @return the cell style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for rows
+ *
+ * @param rowStyleGenerator
+ * the row style generator to set, or <code>null</code> to remove
+ * a previously set generator
+ */
+ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) {
+ this.rowStyleGenerator = rowStyleGenerator;
+ getState().hasRowStyleGenerator = (rowStyleGenerator != null);
+
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for rows
+ *
+ * @return the row style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public RowStyleGenerator getRowStyleGenerator() {
+ return rowStyleGenerator;
+ }
+
+ /**
+ * Adds a row to the underlying container. The order of the parameters
+ * should match the current visible column order.
+ * <p>
+ * Please note that it's generally only safe to use this method during
+ * initialization. After Grid has been initialized and the visible column
+ * order might have been changed, it's better to instead add items directly
+ * to the underlying container and use {@link Item#getItemProperty(Object)}
+ * to make sure each value is assigned to the intended property.
+ *
+ * @param values
+ * the cell values of the new row, in the same order as the
+ * visible column order, not <code>null</code>.
+ * @return the item id of the new row
+ * @throws IllegalArgumentException
+ * if values is null
+ * @throws IllegalArgumentException
+ * if its length does not match the number of visible columns
+ * @throws IllegalArgumentException
+ * if a parameter value is not an instance of the corresponding
+ * property type
+ * @throws UnsupportedOperationException
+ * if the container does not support adding new items
+ */
+ public Object addRow(Object... values) {
+ if (values == null) {
+ throw new IllegalArgumentException("Values cannot be null");
+ }
+
+ Indexed dataSource = getContainerDataSource();
+ List<String> columnOrder = getState(false).columnOrder;
+
+ if (values.length != columnOrder.size()) {
+ throw new IllegalArgumentException("There are "
+ + columnOrder.size() + " visible columns, but "
+ + values.length + " cell values were provided.");
+ }
+
+ // First verify all parameter types
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+
+ Class<?> propertyType = dataSource.getType(propertyId);
+ if (values[i] != null && !propertyType.isInstance(values[i])) {
+ throw new IllegalArgumentException("Parameter " + i + "("
+ + values[i] + ") is not an instance of "
+ + propertyType.getCanonicalName());
+ }
+ }
+
+ Object itemId = dataSource.addItem();
+ try {
+ Item item = dataSource.getItem(itemId);
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+ Property<Object> property = item.getItemProperty(propertyId);
+ property.setValue(values[i]);
+ }
+ } catch (RuntimeException e) {
+ try {
+ dataSource.removeItem(itemId);
+ } catch (Exception e2) {
+ getLogger().log(Level.SEVERE,
+ "Error recovering from exception in addRow", e);
+ }
+ throw e;
+ }
+
+ return itemId;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+ /**
+ * Sets whether or not the item editor UI is enabled for this grid. When the
+ * editor is enabled, the user can open it by double-clicking a row or
+ * hitting enter when a row is focused. The editor can also be opened
+ * programmatically using the {@link #editItem(Object)} method.
+ *
+ * @param isEnabled
+ * <code>true</code> to enable the feature, <code>false</code>
+ * otherwise
+ * @throws IllegalStateException
+ * if an item is currently being edited
+ *
+ * @see #getEditedItemId()
+ */
+ public void setEditorEnabled(boolean isEnabled)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot disable the editor while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ if (isEditorEnabled() != isEnabled) {
+ getState().editorEnabled = isEnabled;
+ }
+ }
+
+ /**
+ * Checks whether the item editor UI is enabled for this grid.
+ *
+ * @return <code>true</code> iff the editor is enabled for this grid
+ *
+ * @see #setEditorEnabled(boolean)
+ * @see #getEditedItemId()
+ */
+ public boolean isEditorEnabled() {
+ return getState(false).editorEnabled;
+ }
+
+ /**
+ * Gets the id of the item that is currently being edited.
+ *
+ * @return the id of the item that is currently being edited, or
+ * <code>null</code> if no item is being edited at the moment
+ */
+ public Object getEditedItemId() {
+ return editedItemId;
+ }
+
+ /**
+ * Gets the field group that is backing the item editor of this grid.
+ *
+ * @return the backing field group
+ */
+ public FieldGroup getEditorFieldGroup() {
+ return editorFieldGroup;
+ }
+
+ /**
+ * Sets the field group that is backing the item editor of this grid.
+ *
+ * @param fieldGroup
+ * the backing field group
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ */
+ public void setEditorFieldGroup(FieldGroup fieldGroup) {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change field group while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ editorFieldGroup = fieldGroup;
+ }
+
+ /**
+ * Returns whether an item is currently being edited in the editor.
+ *
+ * @return true iff the editor is open
+ */
+ public boolean isEditorActive() {
+ return editedItemId != null;
+ }
+
+ private void checkColumnExists(Object propertyId) {
+ if (getColumn(propertyId) == null) {
+ throw new IllegalArgumentException(
+ "There is no column with the property id " + propertyId);
+ }
+ }
+
+ /**
+ * Gets the field component that represents a property in the item editor.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound for any unbound properties.
+ * <p>
+ * Getting a field before the editor has been opened depends on special
+ * support from the {@link FieldGroup} in use. Using this method with a
+ * user-provided <code>FieldGroup</code> might cause {@link BindException}
+ * to be thrown.
+ *
+ * @param propertyId
+ * the property id of the property for which to find the field
+ * @return the bound field, never null
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for the provided property id
+ * @throws BindException
+ * if no field has been configured and there is a problem
+ * building or binding
+ */
+ public Field<?> getEditorField(Object propertyId) {
+ checkColumnExists(propertyId);
+
+ Field<?> editor = editorFieldGroup.getField(propertyId);
+ if (editor == null) {
+ editor = editorFieldGroup.buildAndBind(propertyId);
+ }
+
+ if (editor.getParent() != Grid.this) {
+ assert editor.getParent() == null;
+ editor.setParent(this);
+ }
+ return editor;
+ }
+
+ /**
+ * Opens the editor interface for the provided item.
+ *
+ * @param itemId
+ * the id of the item to edit
+ * @throws IllegalStateException
+ * if the editor is not enabled or already editing an item
+ * @throws IllegalArgumentException
+ * if the {@code itemId} is not in the backing container
+ * @see #setEditorEnabled(boolean)
+ */
+ public void editItem(Object itemId) throws IllegalStateException,
+ IllegalArgumentException {
+ if (!isEditorEnabled()) {
+ throw new IllegalStateException("Item editor is not enabled");
+ } else if (editedItemId != null) {
+ throw new IllegalStateException("Editing item + " + itemId
+ + " failed. Item editor is already editing item "
+ + editedItemId);
+ } else if (!getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Item with id " + itemId
+ + " not found in current container");
+ }
+ editedItemId = itemId;
+ getEditorRpc().bind(getContainerDataSource().indexOfId(itemId));
+ }
+
+ protected void doEditItem() {
+ Item item = getContainerDataSource().getItem(editedItemId);
+
+ editorFieldGroup.setItemDataSource(item);
+
+ for (Column column : getColumns()) {
+ Object propertyId = column.getPropertyId();
+
+ Field<?> editor = getEditorField(propertyId);
+
+ getColumn(propertyId).getState().editorConnector = editor;
+ }
+ }
+
+ /**
+ * Binds the field to the given propertyId. If an item has not been set,
+ * then the binding is postponed until the item is set using
+ * {@link #editItem(Object)}.
+ * <p>
+ * Setting the field to <code>null</code> clears any previously set field,
+ * causing a new field to be created the next time the item editor is
+ * opened.
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind the field to
+ */
+ public void setEditorField(Object propertyId, Field<?> field) {
+ checkColumnExists(propertyId);
+
+ Field<?> oldField = editorFieldGroup.getField(propertyId);
+ if (oldField != null) {
+ editorFieldGroup.unbind(oldField);
+ oldField.setParent(null);
+ }
+
+ if (field != null) {
+ field.setParent(this);
+ editorFieldGroup.bind(field, propertyId);
+ }
+ }
+
+ /**
+ * Saves all changes done to the bound fields.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ *
+ * @see FieldGroup#commit()
+ */
+ public void saveEditor() throws CommitException {
+ editorFieldGroup.commit();
+ }
+
+ /**
+ * Cancels the currently active edit if any. Hides the editor and discards
+ * possible unsaved changes in the editor fields.
+ */
+ public void cancelEditor() {
+ if (isEditorActive()) {
+ getEditorRpc().cancel(
+ getContainerDataSource().indexOfId(editedItemId));
+ doCancelEditor();
+ }
+ }
+
+ protected void doCancelEditor() {
+ editedItemId = null;
+ editorFieldGroup.discard();
+ }
+
+ void resetEditor() {
+ if (isEditorActive()) {
+ /*
+ * Simply force cancel the editing; throwing here would just make
+ * Grid.setContainerDataSource semantics more complicated.
+ */
+ cancelEditor();
+ }
+ for (Field<?> editor : getEditorFields()) {
+ editor.setParent(null);
+ }
+
+ editedItemId = null;
+ editorFieldGroup = new CustomFieldGroup();
+ }
+
+ /**
+ * Gets a collection of all fields bound to the item editor of this grid.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound to any unbound properties.
+ *
+ * @return a collection of all the fields bound to the item editor
+ */
+ Collection<Field<?>> getEditorFields() {
+ Collection<Field<?>> fields = editorFieldGroup.getFields();
+ assert allAttached(fields);
+ return fields;
+ }
+
+ private boolean allAttached(Collection<? extends Component> components) {
+ for (Component component : components) {
+ if (component.getParent() != this) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setEditorFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ editorFieldGroup.setFieldFactory(fieldFactory);
+ }
+
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java b/server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java
new file mode 100644
index 0000000000..8fabded536
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.server.AbstractJavaScriptExtension;
+import com.vaadin.server.JavaScriptCallbackHelper;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.ui.Grid.AbstractRenderer;
+import com.vaadin.ui.JavaScriptFunction;
+
+/**
+ * Base class for Renderers with all client-side logic implemented using
+ * JavaScript.
+ * <p>
+ * When a new JavaScript renderer is initialized in the browser, the framework
+ * will look for a globally defined JavaScript function that will initialize the
+ * renderer. The name of the initialization function is formed by replacing .
+ * with _ in the name of the server-side class. If no such function is defined,
+ * each super class is used in turn until a match is found. The framework will
+ * thus first attempt with <code>com_example_MyRenderer</code> for the
+ * server-side
+ * <code>com.example.MyRenderer extends AbstractJavaScriptRenderer</code> class.
+ * If MyRenderer instead extends <code>com.example.SuperRenderer</code> , then
+ * <code>com_example_SuperRenderer</code> will also be attempted if
+ * <code>com_example_MyRenderer</code> has not been defined.
+ * <p>
+ *
+ * In addition to the general JavaScript extension functionality explained in
+ * {@link AbstractJavaScriptExtension}, this class also provides some
+ * functionality specific for renderers.
+ * <p>
+ * The initialization function will be called with <code>this</code> pointing to
+ * a connector wrapper object providing integration to Vaadin with the following
+ * functions:
+ * <ul>
+ * <li><code>getRowKey(rowIndex)</code> - Gets a unique identifier for the row
+ * at the given index. This identifier can be used on the server to retrieve the
+ * corresponding ItemId using {@link #getItemId(String)}.</li>
+ * </ul>
+ * The connector wrapper also supports these special functions that can be
+ * implemented by the connector:
+ * <ul>
+ * <li><code>render(cell, data)</code> - Callback for rendering the given data
+ * into the given cell. The structure of cell and data are described in separate
+ * sections below. The renderer is required to implement this function.
+ * Corresponds to
+ * {@link com.vaadin.client.renderers.Renderer#render(com.vaadin.client.widget.grid.RendererCellReference, Object)}
+ * .</li>
+ * <li><code>init(cell)</code> - Prepares a cell for rendering. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#init(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>destory(cell)</code> - Allows the renderer to release resources
+ * allocate for a cell that will no longer be used. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#destroy(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>onActivate(cell)</code> - Called when the cell is activated by the
+ * user e.g. by double clicking on the cell or pressing enter with the cell
+ * focused. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onActivate(com.vaadin.client.widget.grid.CellReference)}
+ * .</li>
+ * <li><code>getConsumedEvents()</code> - Returns a JavaScript array of event
+ * names that should cause onBrowserEvent to be invoked whenever an event is
+ * fired for a cell managed by this renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#getConsumedEvents()}.</li>
+ * <li><code>onBrowserEvent(cell, event)</code> - Called by Grid when an event
+ * of a type returned by getConsumedEvents is fired for a cell managed by this
+ * renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onBrowserEvent(com.vaadin.client.widget.grid.CellReference, com.google.gwt.dom.client.NativeEvent)}
+ * .</li>
+ * </ul>
+ *
+ * <p>
+ * The cell object passed to functions defined by the renderer has these
+ * properties:
+ * <ul>
+ * <li><code>element</code> - The DOM element corresponding to this cell.
+ * Readonly.</li>
+ * <li><code>rowIndex</code> - The current index of the row of this cell.
+ * Readonly.</li>
+ * <li><code>columnIndex</code> - The current index of the column of this cell.
+ * Readonly.</li>
+ * <li><code>colSpan</code> - The number of columns spanned by this cell. Only
+ * supported in the object passed to the <code>render</code> function - other
+ * functions should not use the property. Readable and writable.
+ * </ul>
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public abstract class AbstractJavaScriptRenderer<T> extends AbstractRenderer<T> {
+ private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper(
+ this);
+
+ protected AbstractJavaScriptRenderer(Class<T> presentationType) {
+ super(presentationType);
+ }
+
+ @Override
+ protected <R extends ServerRpc> void registerRpc(R implementation,
+ Class<R> rpcInterfaceType) {
+ super.registerRpc(implementation, rpcInterfaceType);
+ callbackHelper.registerRpc(rpcInterfaceType);
+ }
+
+ /**
+ * Register a {@link JavaScriptFunction} that can be called from the
+ * JavaScript using the provided name. A JavaScript function with the
+ * provided name will be added to the connector wrapper object (initially
+ * available as <code>this</code>). Calling that JavaScript function will
+ * cause the call method in the registered {@link JavaScriptFunction} to be
+ * invoked with the same arguments.
+ *
+ * @param functionName
+ * the name that should be used for client-side callback
+ * @param function
+ * the {@link JavaScriptFunction} object that will be invoked
+ * when the JavaScript function is called
+ */
+ protected void addFunction(String functionName, JavaScriptFunction function) {
+ callbackHelper.registerCallback(functionName, function);
+ }
+
+ /**
+ * Invoke a named function that the connector JavaScript has added to the
+ * JavaScript connector wrapper object. The arguments should only contain
+ * data types that can be represented in JavaScript including primitives,
+ * their boxed types, arrays, String, List, Set, Map, Connector and
+ * JavaBeans.
+ *
+ * @param name
+ * the name of the function
+ * @param arguments
+ * function arguments
+ */
+ protected void callFunction(String name, Object... arguments) {
+ callbackHelper.invokeCallback(name, arguments);
+ }
+
+ @Override
+ protected JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/ButtonRenderer.java b/server/src/com/vaadin/ui/renderer/ButtonRenderer.java
new file mode 100644
index 0000000000..b0819794c0
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/ButtonRenderer.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+/**
+ * A Renderer that displays a button with a textual caption. The value of the
+ * corresponding property is used as the caption. Click listeners can be added
+ * to the renderer, invoked when any of the rendered buttons is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ButtonRenderer extends ClickableRenderer<String> {
+
+ /**
+ * Creates a new button renderer.
+ */
+ public ButtonRenderer() {
+ super(String.class);
+ }
+
+ /**
+ * Creates a new button renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ */
+ public ButtonRenderer(RendererClickListener listener) {
+ this();
+ addClickListener(listener);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/ClickableRenderer.java b/server/src/com/vaadin/ui/renderer/ClickableRenderer.java
new file mode 100644
index 0000000000..d640ce8b71
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/ClickableRenderer.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.event.ConnectorEventListener;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.grid.renderers.RendererClickRpc;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.AbstractRenderer;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * An abstract superclass for Renderers that render clickable items. Click
+ * listeners can be added to a renderer to be notified when any of the rendered
+ * items is clicked.
+ *
+ * @param <T>
+ * the type presented by the renderer
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ClickableRenderer<T> extends AbstractRenderer<T> {
+
+ /**
+ * An interface for listening to {@link RendererClickEvent renderer click
+ * events}.
+ *
+ * @see {@link ButtonRenderer#addClickListener(RendererClickListener)}
+ */
+ public interface RendererClickListener extends ConnectorEventListener {
+
+ static final Method CLICK_METHOD = ReflectTools.findMethod(
+ RendererClickListener.class, "click", RendererClickEvent.class);
+
+ /**
+ * Called when a rendered button is clicked.
+ *
+ * @param event
+ * the event representing the click
+ */
+ void click(RendererClickEvent event);
+ }
+
+ /**
+ * An event fired when a button rendered by a ButtonRenderer is clicked.
+ */
+ public static class RendererClickEvent extends ClickEvent {
+
+ private Object itemId;
+ private Column column;
+
+ protected RendererClickEvent(Grid source, Object itemId, Column column,
+ MouseEventDetails mouseEventDetails) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ this.column = column;
+ }
+
+ /**
+ * Returns the item ID of the row where the click event originated.
+ *
+ * @return the item ID of the clicked row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns the {@link Column} where the click event originated.
+ *
+ * @return the column of the click event
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the property ID where the click event originated.
+ *
+ * @return the property ID of the clicked cell
+ */
+ public Object getPropertyId() {
+ return column.getPropertyId();
+ }
+ }
+
+ protected ClickableRenderer(Class<T> presentationType) {
+ super(presentationType);
+ registerRpc(new RendererClickRpc() {
+ @Override
+ public void click(String rowKey, String columnId,
+ MouseEventDetails mouseDetails) {
+ fireEvent(new RendererClickEvent(getParentGrid(),
+ getItemId(rowKey), getColumn(columnId), mouseDetails));
+ }
+ });
+ }
+
+ /**
+ * Adds a click listener to this button renderer. The listener is invoked
+ * every time one of the buttons rendered by this renderer is clicked.
+ *
+ * @param listener
+ * the click listener to be added
+ */
+ public void addClickListener(RendererClickListener listener) {
+ addListener(RendererClickEvent.class, listener,
+ RendererClickListener.CLICK_METHOD);
+ }
+
+ /**
+ * Removes the given click listener from this renderer.
+ *
+ * @param listener
+ * the click listener to be removed
+ */
+ public void removeClickListener(RendererClickListener listener) {
+ removeListener(RendererClickEvent.class, listener);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/DateRenderer.java b/server/src/com/vaadin/ui/renderer/DateRenderer.java
new file mode 100644
index 0000000000..d3d2df573d
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/DateRenderer.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting date values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DateRenderer extends AbstractRenderer<Date> {
+ private final Locale locale;
+ private final String formatString;
+ private final DateFormat dateFormat;
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the default locale.
+ */
+ public DateRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ super(Date.class);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("locale may not be null");
+ }
+
+ this.locale = locale;
+ this.formatString = formatString;
+ dateFormat = null;
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException {
+ super(Date.class);
+ if (dateFormat == null) {
+ throw new IllegalArgumentException("date format may not be null");
+ }
+
+ locale = null;
+ formatString = null;
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public JsonValue encode(Date value) {
+ String dateString;
+ if (dateFormat != null) {
+ dateString = dateFormat.format(value);
+ } else {
+ dateString = String.format(locale, formatString, value);
+ }
+ return encode(dateString, String.class);
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (dateFormat != null) {
+ fieldInfo = "dateFormat: " + dateFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/HtmlRenderer.java b/server/src/com/vaadin/ui/renderer/HtmlRenderer.java
new file mode 100644
index 0000000000..02d153dedf
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/HtmlRenderer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting HTML content.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class HtmlRenderer extends AbstractRenderer<String> {
+ /**
+ * Creates a new HTML renderer.
+ */
+ public HtmlRenderer() {
+ super(String.class);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/ImageRenderer.java b/server/src/com/vaadin/ui/renderer/ImageRenderer.java
new file mode 100644
index 0000000000..3ef3eed3e5
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/ImageRenderer.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ResourceReference;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.shared.communication.URLReference;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting images.
+ * <p>
+ * The image for each rendered cell is read from a Resource-typed property in
+ * the data source. Only {@link ExternalResource}s and {@link ThemeResource}s
+ * are currently supported.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ImageRenderer extends ClickableRenderer<Resource> {
+
+ /**
+ * Creates a new image renderer.
+ */
+ public ImageRenderer() {
+ super(Resource.class);
+ }
+
+ /**
+ * Creates a new image renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ */
+ public ImageRenderer(RendererClickListener listener) {
+ this();
+ addClickListener(listener);
+ }
+
+ @Override
+ public JsonValue encode(Resource resource) {
+ if (!(resource instanceof ExternalResource || resource instanceof ThemeResource)) {
+ throw new IllegalArgumentException(
+ "ImageRenderer only supports ExternalResource and ThemeResource ("
+ + resource.getClass().getSimpleName() + "given )");
+ }
+
+ return encode(ResourceReference.create(resource, this, null),
+ URLReference.class);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/NumberRenderer.java b/server/src/com/vaadin/ui/renderer/NumberRenderer.java
new file mode 100644
index 0000000000..3406e1837a
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/NumberRenderer.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting number values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class NumberRenderer extends AbstractRenderer<Number> {
+ private final Locale locale;
+ private final NumberFormat numberFormat;
+ private final String formatString;
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the number's natural string
+ * representation in the default locale.
+ */
+ public NumberRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat)
+ throws IllegalArgumentException {
+ super(Number.class);
+
+ if (numberFormat == null) {
+ throw new IllegalArgumentException("Number format may not be null");
+ }
+
+ locale = null;
+ this.numberFormat = numberFormat;
+ formatString = null;
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the given format string in the
+ * default locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the given format string in the
+ * given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to present numbers
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString, Locale locale) {
+ super(Number.class);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("Format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("Locale may not be null");
+ }
+
+ this.locale = locale;
+ numberFormat = null;
+ this.formatString = formatString;
+ }
+
+ @Override
+ public JsonValue encode(Number value) {
+ String stringValue;
+ if (formatString != null && locale != null) {
+ stringValue = String.format(locale, formatString, value);
+ } else if (numberFormat != null) {
+ stringValue = numberFormat.format(value);
+ } else {
+ throw new IllegalStateException(String.format("Internal bug: "
+ + "%s is in an illegal state: "
+ + "[locale: %s, numberFormat: %s, formatString: %s]",
+ getClass().getSimpleName(), locale, numberFormat,
+ formatString));
+ }
+ return encode(stringValue, String.class);
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (numberFormat != null) {
+ fieldInfo = "numberFormat: " + numberFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/ObjectRenderer.java b/server/src/com/vaadin/ui/renderer/ObjectRenderer.java
new file mode 100644
index 0000000000..9f8b44162c
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/ObjectRenderer.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for displaying an object to a string using the
+ * {@link Object#toString()} method.
+ * <p>
+ * If the object is <code>null</code>, then it is rendered as an empty string
+ * instead.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class ObjectRenderer extends AbstractRenderer<Object> {
+
+ /**
+ * Creates a new <code>toString</code> renderer.
+ */
+ public ObjectRenderer() {
+ super(Object.class);
+ }
+
+ @Override
+ public JsonValue encode(Object value) {
+ String text = (value != null) ? value.toString() : "";
+ return super.encode(text);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java b/server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java
new file mode 100644
index 0000000000..9bdc0b299a
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer that represents a double values as a graphical progress bar.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ProgressBarRenderer extends AbstractRenderer<Double> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public ProgressBarRenderer() {
+ super(Double.class);
+ }
+
+ @Override
+ public JsonValue encode(Double value) {
+ if (value != null) {
+ value = Math.max(Math.min(value, 1), 0);
+ }
+ return super.encode(value);
+ }
+}
diff --git a/server/src/com/vaadin/ui/renderer/Renderer.java b/server/src/com/vaadin/ui/renderer/Renderer.java
new file mode 100644
index 0000000000..cab1cdfe3c
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/Renderer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.Extension;
+
+import elemental.json.JsonValue;
+
+/**
+ * A ClientConnector for controlling client-side
+ * {@link com.vaadin.client.widget.grid.Renderer Grid renderers}. Renderers
+ * currently extend the Extension interface, but this fact should be regarded as
+ * an implementation detail and subject to change in a future major or minor
+ * Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> extends Extension {
+
+ /**
+ * Returns the class literal corresponding to the presentation type T.
+ *
+ * @return the class literal of T
+ */
+ Class<T> getPresentationType();
+
+ /**
+ * Encodes the given value into a {@link JsonValue}.
+ *
+ * @param value
+ * the value to encode
+ * @return a JSON representation of the given value
+ */
+ JsonValue encode(T value);
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void remove();
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void setParent(ClientConnector parent);
+}
diff --git a/server/src/com/vaadin/ui/renderer/TextRenderer.java b/server/src/com/vaadin/ui/renderer/TextRenderer.java
new file mode 100644
index 0000000000..154a09ccd8
--- /dev/null
+++ b/server/src/com/vaadin/ui/renderer/TextRenderer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.ui.renderer;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting simple plain-text string values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class TextRenderer extends AbstractRenderer<String> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public TextRenderer() {
+ super(String.class);
+ }
+}
diff --git a/server/src/com/vaadin/ui/themes/Reindeer.java b/server/src/com/vaadin/ui/themes/Reindeer.java
index 6eeebd8a03..e0ab792a15 100644
--- a/server/src/com/vaadin/ui/themes/Reindeer.java
+++ b/server/src/com/vaadin/ui/themes/Reindeer.java
@@ -15,14 +15,6 @@
*/
package com.vaadin.ui.themes;
-import com.vaadin.ui.CssLayout;
-import com.vaadin.ui.FormLayout;
-import com.vaadin.ui.GridLayout;
-import com.vaadin.ui.HorizontalLayout;
-import com.vaadin.ui.HorizontalSplitPanel;
-import com.vaadin.ui.VerticalLayout;
-import com.vaadin.ui.VerticalSplitPanel;
-
public class Reindeer extends BaseTheme {
public static final String THEME_NAME = "reindeer";
@@ -90,6 +82,18 @@ public class Reindeer extends BaseTheme {
/***************************************************************************
*
+ * ProgressBar Styles
+ *
+ **************************************************************************/
+
+ /**
+ * Displays the progress bar with a static background, instead of an
+ * animated one.
+ */
+ public static final String PROGRESSBAR_STATIC = "static";
+
+ /***************************************************************************
+ *
* SplitPanel styles
*
**************************************************************************/
diff --git a/server/src/com/vaadin/ui/themes/Runo.java b/server/src/com/vaadin/ui/themes/Runo.java
index 11f1bae682..6f8d5f37d9 100644
--- a/server/src/com/vaadin/ui/themes/Runo.java
+++ b/server/src/com/vaadin/ui/themes/Runo.java
@@ -59,6 +59,18 @@ public class Runo extends BaseTheme {
/***************************************************************************
*
+ * ProgressBar Styles
+ *
+ **************************************************************************/
+
+ /**
+ * Displays the progress bar with a static background, instead of an
+ * animated one.
+ */
+ public static final String PROGRESSBAR_STATIC = "static";
+
+ /***************************************************************************
+ *
* TabSheet styles
*
**************************************************************************/
diff --git a/server/tests/src/com/vaadin/data/fieldgroup/FieldGroupTests.java b/server/tests/src/com/vaadin/data/fieldgroup/FieldGroupTests.java
index fc267fc7da..dce9f656b9 100644
--- a/server/tests/src/com/vaadin/data/fieldgroup/FieldGroupTests.java
+++ b/server/tests/src/com/vaadin/data/fieldgroup/FieldGroupTests.java
@@ -2,6 +2,7 @@ package com.vaadin.data.fieldgroup;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
import static org.mockito.Mockito.mock;
import org.junit.Assert;
@@ -44,6 +45,13 @@ public class FieldGroupTests {
sut.bind(null, "foobar");
}
+ public void canUnbindWithoutItem() {
+ sut.bind(field, "foobar");
+
+ sut.unbind(field);
+ assertThat(sut.getField("foobar"), is(nullValue()));
+ }
+
@Test
public void wrapInTransactionalProperty_provideCustomImpl_customTransactionalWrapperIsUsed() {
Bean bean = new Bean();
diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java
index 8d2654b39b..3c30b41d39 100644
--- a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java
+++ b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java
@@ -8,11 +8,17 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
import org.junit.Assert;
import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Item;
import com.vaadin.data.util.NestedMethodPropertyTest.Address;
+import com.vaadin.data.util.filter.Compare;
/**
* Test basic functionality of BeanItemContainer.
@@ -742,6 +748,184 @@ public class BeanItemContainerTest extends AbstractBeanContainerTestBase {
.getValue());
}
+ public void testItemAddedEvent() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem(bean);
+
+ EasyMock.verify(addListener);
+ }
+
+ public void testItemAddedEvent_AddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItem(bean);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemAddedEvent_addItemAt_IndexOfAddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItemAt(1, new Person(""));
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemAddedEvent_addItemAfter_IndexOfAddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItemAfter(bean, new Person(""));
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemAddedEvent_amountOfAddedItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), new Person(
+ "John"));
+
+ container.addAll(beans);
+
+ assertEquals(2, capturedEvent.getValue().getAddedItemsCount());
+ }
+
+ public void testItemAddedEvent_someItemsAreFiltered_amountOfAddedItemsIsReducedByAmountOfFilteredItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), new Person(
+ "John"));
+ container.addFilter(new Compare.Equal("name", "John"));
+
+ container.addAll(beans);
+
+ assertEquals(1, capturedEvent.getValue().getAddedItemsCount());
+ }
+
+ public void testItemAddedEvent_someItemsAreFiltered_addedItemIsTheFirstVisibleItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), bean);
+ container.addFilter(new Compare.Equal("name", "John"));
+
+ container.addAll(beans);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemRemovedEvent() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener.containerItemSetChange(EasyMock
+ .isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(bean);
+
+ EasyMock.verify(removeListener);
+ }
+
+ public void testItemRemovedEvent_RemovedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(bean);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addItem(new Person("Jack"));
+ Person secondBean = new Person("John");
+ container.addItem(secondBean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondBean);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addItem(new Person("Jack"));
+ container.addItem(new Person("John"));
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ BeanItemContainer<Person> container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
+
public void testAddNestedContainerBeanBeforeData() {
BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>(
NestedMethodPropertyTest.Person.class);
diff --git a/server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java b/server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java
new file mode 100644
index 0000000000..bfa77eab52
--- /dev/null
+++ b/server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Item;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.filter.Compare;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+public class GeneratedPropertyContainerTest {
+
+ GeneratedPropertyContainer container;
+ Indexed wrappedContainer;
+ private static double MILES_CONVERSION = 0.6214d;
+
+ private class GeneratedPropertyListener implements
+ PropertySetChangeListener {
+
+ private int callCount = 0;
+
+ public int getCallCount() {
+ return callCount;
+ }
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ ++callCount;
+ assertEquals(
+ "Container for event was not GeneratedPropertyContainer",
+ event.getContainer(), container);
+ }
+ }
+
+ private class GeneratedItemSetListener implements ItemSetChangeListener {
+
+ private int callCount = 0;
+
+ public int getCallCount() {
+ return callCount;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ ++callCount;
+ assertEquals(
+ "Container for event was not GeneratedPropertyContainer",
+ event.getContainer(), container);
+ }
+ }
+
+ @Before
+ public void setUp() {
+ container = new GeneratedPropertyContainer(createContainer());
+ }
+
+ @Test
+ public void testSimpleGeneratedProperty() {
+ container.addGeneratedProperty("hello",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return "Hello World!";
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+
+ Object itemId = container.addItem();
+ assertEquals("Expected value not in item.", container.getItem(itemId)
+ .getItemProperty("hello").getValue(), "Hello World!");
+ }
+
+ @Test
+ public void testSortableProperties() {
+ container.addGeneratedProperty("baz",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return item.getItemProperty("foo").getValue() + " "
+ + item.getItemProperty("bar").getValue();
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ @Override
+ public SortOrder[] getSortProperties(SortOrder order) {
+ SortOrder[] sortOrder = new SortOrder[1];
+ sortOrder[0] = new SortOrder("bar", order
+ .getDirection());
+ return sortOrder;
+ }
+ });
+
+ container.sort(new Object[] { "baz" }, new boolean[] { true });
+ assertEquals("foo 0", container.getItem(container.getIdByIndex(0))
+ .getItemProperty("baz").getValue());
+
+ container.sort(new Object[] { "baz" }, new boolean[] { false });
+ assertEquals("foo 10", container.getItem(container.getIdByIndex(0))
+ .getItemProperty("baz").getValue());
+ }
+
+ @Test
+ public void testOverrideSortableProperties() {
+
+ assertTrue(container.getSortableContainerPropertyIds().contains("bar"));
+
+ container.addGeneratedProperty("bar",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return item.getItemProperty("foo").getValue() + " "
+ + item.getItemProperty("bar").getValue();
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+
+ assertFalse(container.getSortableContainerPropertyIds().contains("bar"));
+ }
+
+ @Test
+ public void testFilterByMiles() {
+ container.addGeneratedProperty("miles",
+ new PropertyValueGenerator<Double>() {
+
+ @Override
+ public Double getValue(Item item, Object itemId,
+ Object propertyId) {
+ return (Double) item.getItemProperty("km").getValue()
+ * MILES_CONVERSION;
+ }
+
+ @Override
+ public Class<Double> getType() {
+ return Double.class;
+ }
+
+ @Override
+ public Filter modifyFilter(Filter filter)
+ throws UnsupportedFilterException {
+ if (filter instanceof Compare.LessOrEqual) {
+ Double value = (Double) ((Compare.LessOrEqual) filter)
+ .getValue();
+ value = value / MILES_CONVERSION;
+ return new Compare.LessOrEqual("km", value);
+ }
+ return super.modifyFilter(filter);
+ }
+ });
+
+ for (Object itemId : container.getItemIds()) {
+ Item item = container.getItem(itemId);
+ Double km = (Double) item.getItemProperty("km").getValue();
+ Double miles = (Double) item.getItemProperty("miles").getValue();
+ assertTrue(miles.equals(km * MILES_CONVERSION));
+ }
+
+ Filter filter = new Compare.LessOrEqual("miles", MILES_CONVERSION);
+ container.addContainerFilter(filter);
+ for (Object itemId : container.getItemIds()) {
+ Item item = container.getItem(itemId);
+ assertTrue("Item did not pass original filter.",
+ filter.passesFilter(itemId, item));
+ }
+
+ assertTrue(container.getContainerFilters().contains(filter));
+ container.removeContainerFilter(filter);
+ assertFalse(container.getContainerFilters().contains(filter));
+
+ boolean allPass = true;
+ for (Object itemId : container.getItemIds()) {
+ Item item = container.getItem(itemId);
+ if (!filter.passesFilter(itemId, item)) {
+ allPass = false;
+ }
+ }
+
+ if (allPass) {
+ fail("Removing filter did not introduce any previous filtered items");
+ }
+ }
+
+ @Test
+ public void testPropertySetChangeNotifier() {
+ GeneratedPropertyListener listener = new GeneratedPropertyListener();
+ GeneratedPropertyListener removedListener = new GeneratedPropertyListener();
+ container.addPropertySetChangeListener(listener);
+ container.addPropertySetChangeListener(removedListener);
+
+ container.addGeneratedProperty("foo",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return "";
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+
+ // Adding property to wrapped container should cause an event
+ wrappedContainer.addContainerProperty("baz", String.class, "");
+ container.removePropertySetChangeListener(removedListener);
+ container.removeGeneratedProperty("foo");
+
+ assertEquals("Listener was not called correctly.", 3,
+ listener.getCallCount());
+ assertEquals("Removed listener was not called correctly.", 2,
+ removedListener.getCallCount());
+ }
+
+ @Test
+ public void testItemSetChangeNotifier() {
+ GeneratedItemSetListener listener = new GeneratedItemSetListener();
+ container.addItemSetChangeListener(listener);
+
+ container.sort(new Object[] { "foo" }, new boolean[] { true });
+ container.sort(new Object[] { "foo" }, new boolean[] { false });
+
+ assertEquals("Listener was not called correctly.", 2,
+ listener.getCallCount());
+
+ }
+
+ @Test
+ public void testRemoveProperty() {
+ container.removeContainerProperty("foo");
+ assertFalse("Container contained removed property", container
+ .getContainerPropertyIds().contains("foo"));
+ assertTrue("Wrapped container did not contain removed property",
+ wrappedContainer.getContainerPropertyIds().contains("foo"));
+
+ assertFalse(container.getItem(container.firstItemId())
+ .getItemPropertyIds().contains("foo"));
+
+ container.addContainerProperty("foo", null, null);
+ assertTrue("Container did not contain returned property", container
+ .getContainerPropertyIds().contains("foo"));
+ }
+
+ private Indexed createContainer() {
+ wrappedContainer = new IndexedContainer();
+ wrappedContainer.addContainerProperty("foo", String.class, "foo");
+ wrappedContainer.addContainerProperty("bar", Integer.class, 0);
+ // km contains double values from 0.0 to 2.0
+ wrappedContainer.addContainerProperty("km", Double.class, 0);
+
+ for (int i = 0; i <= 10; ++i) {
+ Object itemId = wrappedContainer.addItem();
+ Item item = wrappedContainer.getItem(itemId);
+ item.getItemProperty("foo").setValue("foo");
+ item.getItemProperty("bar").setValue(i);
+ item.getItemProperty("km").setValue(i / 5.0d);
+ }
+
+ return wrappedContainer;
+ }
+
+}
diff --git a/server/tests/src/com/vaadin/data/util/IndexedContainerTest.java b/server/tests/src/com/vaadin/data/util/IndexedContainerTest.java
index 2f64e7c797..5828ac88cc 100644
--- a/server/tests/src/com/vaadin/data/util/IndexedContainerTest.java
+++ b/server/tests/src/com/vaadin/data/util/IndexedContainerTest.java
@@ -2,8 +2,13 @@ package com.vaadin.data.util;
import java.util.List;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
import org.junit.Assert;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Item;
public class IndexedContainerTest extends AbstractInMemoryContainerTestBase {
@@ -271,6 +276,145 @@ public class IndexedContainerTest extends AbstractInMemoryContainerTestBase {
counter.assertNone();
}
+ public void testItemAdd_idSequence() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId;
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(1), itemId);
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(2), itemId);
+
+ itemId = container.addItemAfter(null);
+ assertEquals(Integer.valueOf(3), itemId);
+
+ itemId = container.addItemAt(2);
+ assertEquals(Integer.valueOf(4), itemId);
+ }
+
+ public void testItemAddRemove_idSequence() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId;
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(1), itemId);
+
+ container.removeItem(itemId);
+
+ itemId = container.addItem();
+ assertEquals(
+ "Id sequence should continue from the previous value even if an item is removed",
+ Integer.valueOf(2), itemId);
+ }
+
+ public void testItemAddedEvent() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem();
+
+ EasyMock.verify(addListener);
+ }
+
+ public void testItemAddedEvent_AddedItem() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItem();
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemAddedEvent_IndexOfAddedItem() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ container.addItem();
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItemAt(1);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemRemovedEvent() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener.containerItemSetChange(EasyMock
+ .isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ EasyMock.verify(removeListener);
+ }
+
+ public void testItemRemovedEvent_RemovedItem() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ IndexedContainer container = new IndexedContainer();
+ container.addItem();
+ Object secondItemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondItemId);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ IndexedContainer container = new IndexedContainer();
+ container.addItem();
+ container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ IndexedContainer container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
+
// Ticket 8028
public void testGetItemIdsRangeIndexOutOfBounds() {
IndexedContainer ic = new IndexedContainer();
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
new file mode 100644
index 0000000000..9ecf131c5b
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.data.util.IndexedContainer;
+
+public class DataProviderExtension {
+ private RpcDataProviderExtension dataProvider;
+ private DataProviderKeyMapper keyMapper;
+ private Container.Indexed container;
+
+ private static final Object ITEM_ID1 = "itemid1";
+ private static final Object ITEM_ID2 = "itemid2";
+ private static final Object ITEM_ID3 = "itemid3";
+
+ private static final Object PROPERTY_ID1_STRING = "property1";
+
+ @Before
+ public void setup() {
+ container = new IndexedContainer();
+ populate(container);
+
+ dataProvider = new RpcDataProviderExtension(container);
+ keyMapper = dataProvider.getKeyMapper();
+ }
+
+ private static void populate(Indexed container) {
+ container.addContainerProperty(PROPERTY_ID1_STRING, String.class, "");
+ for (Object itemId : Arrays.asList(ITEM_ID1, ITEM_ID2, ITEM_ID3)) {
+ final Item item = container.addItem(itemId);
+ @SuppressWarnings("unchecked")
+ final Property<String> stringProperty = item
+ .getItemProperty(PROPERTY_ID1_STRING);
+ stringProperty.setValue(itemId.toString());
+ }
+ }
+
+ @Test
+ public void pinBasics() {
+ assertFalse("itemId1 should not start as pinned",
+ keyMapper.isPinned(ITEM_ID2));
+
+ keyMapper.pin(ITEM_ID1);
+ assertTrue("itemId1 should now be pinned", keyMapper.isPinned(ITEM_ID1));
+
+ keyMapper.unpin(ITEM_ID1);
+ assertFalse("itemId1 should not be pinned anymore",
+ keyMapper.isPinned(ITEM_ID2));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void doublePinning() {
+ keyMapper.pin(ITEM_ID1);
+ keyMapper.pin(ITEM_ID1);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void nonexistentUnpin() {
+ keyMapper.unpin(ITEM_ID1);
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java
new file mode 100644
index 0000000000..70c73eb516
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.util.BeanItemContainer;
+import com.vaadin.data.util.MethodProperty.MethodException;
+import com.vaadin.tests.data.bean.Person;
+import com.vaadin.ui.Grid;
+
+public class GridAddRowBuiltinContainerTest {
+ Grid grid = new Grid();
+ Container.Indexed container;
+
+ @Before
+ public void setUp() {
+ container = grid.getContainerDataSource();
+
+ grid.addColumn("myColumn");
+ }
+
+ @Test
+ public void testSimpleCase() {
+ Object itemId = grid.addRow("Hello");
+
+ Assert.assertEquals(Integer.valueOf(1), itemId);
+
+ Assert.assertEquals("There should be one item in the container", 1,
+ container.size());
+
+ Assert.assertEquals("Hello",
+ container.getItem(itemId).getItemProperty("myColumn")
+ .getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullParameter() {
+ // cast to Object[] to distinguish from one null varargs value
+ grid.addRow((Object[]) null);
+ }
+
+ @Test
+ public void testNullValue() {
+ // cast to Object to distinguish from a null varargs array
+ Object itemId = grid.addRow((Object) null);
+
+ Assert.assertEquals(null,
+ container.getItem(itemId).getItemProperty("myColumn")
+ .getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddInvalidType() {
+ grid.addRow(Integer.valueOf(5));
+ }
+
+ @Test
+ public void testMultipleProperties() {
+ grid.addColumn("myOther", Integer.class);
+
+ Object itemId = grid.addRow("Hello", Integer.valueOf(3));
+
+ Item item = container.getItem(itemId);
+ Assert.assertEquals("Hello", item.getItemProperty("myColumn")
+ .getValue());
+ Assert.assertEquals(Integer.valueOf(3), item.getItemProperty("myOther")
+ .getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidPropertyAmount() {
+ grid.addRow("Hello", Integer.valueOf(3));
+ }
+
+ @Test
+ public void testRemovedColumn() {
+ grid.addColumn("myOther", Integer.class);
+ grid.removeColumn("myColumn");
+
+ grid.addRow(Integer.valueOf(3));
+
+ Item item = container.getItem(Integer.valueOf(1));
+ Assert.assertEquals("Default value should be used for removed column",
+ "", item.getItemProperty("myColumn").getValue());
+ Assert.assertEquals(Integer.valueOf(3), item.getItemProperty("myOther")
+ .getValue());
+ }
+
+ @Test
+ public void testMultiplePropertiesAfterReorder() {
+ grid.addColumn("myOther", Integer.class);
+
+ grid.setColumnOrder("myOther", "myColumn");
+
+ grid.addRow(Integer.valueOf(3), "Hello");
+
+ Item item = container.getItem(Integer.valueOf(1));
+ Assert.assertEquals("Hello", item.getItemProperty("myColumn")
+ .getValue());
+ Assert.assertEquals(Integer.valueOf(3), item.getItemProperty("myOther")
+ .getValue());
+ }
+
+ @Test
+ public void testInvalidType_NothingAdded() {
+ try {
+ grid.addRow(Integer.valueOf(5));
+
+ // Can't use @Test(expect = Foo.class) since we also want to verify
+ // state after exception was thrown
+ Assert.fail("Adding wrong type should throw ClassCastException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("No row should have been added", 0,
+ container.size());
+ }
+ }
+
+ @Test
+ public void testUnsupportingContainer() {
+ setContainerRemoveColumns(new BeanItemContainer<Person>(Person.class));
+ try {
+
+ grid.addRow("name");
+
+ // Can't use @Test(expect = Foo.class) since we also want to verify
+ // state after exception was thrown
+ Assert.fail("Adding to BeanItemContainer container should throw UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ Assert.assertEquals("No row should have been added", 0,
+ container.size());
+ }
+ }
+
+ @Test
+ public void testCustomContainer() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class) {
+ @Override
+ public Object addItem() {
+ BeanItem<Person> item = addBean(new Person());
+ return getBeanIdResolver().getIdForBean(item.getBean());
+ }
+ };
+
+ setContainerRemoveColumns(container);
+
+ grid.addRow("name");
+
+ Assert.assertEquals(1, container.size());
+
+ Assert.assertEquals("name", container.getIdByIndex(0).getFirstName());
+ }
+
+ @Test
+ public void testSetterThrowing() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class) {
+ @Override
+ public Object addItem() {
+ BeanItem<Person> item = addBean(new Person() {
+ @Override
+ public void setFirstName(String firstName) {
+ if ("name".equals(firstName)) {
+ throw new RuntimeException(firstName);
+ } else {
+ super.setFirstName(firstName);
+ }
+ }
+ });
+ return getBeanIdResolver().getIdForBean(item.getBean());
+ }
+ };
+
+ setContainerRemoveColumns(container);
+
+ try {
+
+ grid.addRow("name");
+
+ // Can't use @Test(expect = Foo.class) since we also want to verify
+ // state after exception was thrown
+ Assert.fail("Adding row should throw MethodException");
+ } catch (MethodException e) {
+ Assert.assertEquals("Got the wrong exception", "name", e.getCause()
+ .getMessage());
+
+ Assert.assertEquals("There should be no rows in the container", 0,
+ container.size());
+ }
+ }
+
+ private void setContainerRemoveColumns(BeanItemContainer<Person> container) {
+ // Remove predefined column so we can change container
+ grid.removeAllColumns();
+ grid.setContainerDataSource(container);
+ grid.removeAllColumns();
+ grid.addColumn("firstName");
+ }
+
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java
new file mode 100644
index 0000000000..97f0355b4b
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.Grid;
+
+public class GridColumnAddingAndRemovingTest {
+
+ Grid grid = new Grid();
+ Container.Indexed container;
+
+ @Before
+ public void setUp() {
+ container = grid.getContainerDataSource();
+ container.addItem();
+ }
+
+ @Test
+ public void testAddColumn() {
+ grid.addColumn("foo");
+
+ Property<?> property = container.getContainerProperty(
+ container.firstItemId(), "foo");
+ assertEquals(property.getType(), String.class);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddColumnTwice() {
+ grid.addColumn("foo");
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testAddRemoveAndAddAgainColumn() {
+ grid.addColumn("foo");
+ grid.removeColumn("foo");
+
+ // Removing a column, doesn't remove the property
+ Property<?> property = container.getContainerProperty(
+ container.firstItemId(), "foo");
+ assertEquals(property.getType(), String.class);
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testAddNumberColumns() {
+ grid.addColumn("bar", Integer.class);
+ grid.addColumn("baz", Double.class);
+
+ Property<?> property = container.getContainerProperty(
+ container.firstItemId(), "bar");
+ assertEquals(property.getType(), Integer.class);
+ assertEquals(null, property.getValue());
+ property = container.getContainerProperty(container.firstItemId(),
+ "baz");
+ assertEquals(property.getType(), Double.class);
+ assertEquals(null, property.getValue());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddDifferentTypeColumn() {
+ grid.addColumn("foo");
+ grid.removeColumn("foo");
+ grid.addColumn("foo", Integer.class);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddColumnToNonDefaultContainer() {
+ grid.setContainerDataSource(new IndexedContainer());
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testAddColumnForExistingProperty() {
+ grid.addColumn("bar");
+ IndexedContainer container2 = new IndexedContainer();
+ container2.addContainerProperty("foo", Integer.class, 0);
+ container2.addContainerProperty("bar", String.class, "");
+ grid.setContainerDataSource(container2);
+ assertNull("Grid should not have a column for property foo",
+ grid.getColumn("foo"));
+ assertNotNull("Grid did should have a column for property bar",
+ grid.getColumn("bar"));
+ for (Grid.Column column : grid.getColumns()) {
+ assertNotNull("Grid getColumns returned a null value", column);
+ }
+
+ grid.removeAllColumns();
+ grid.addColumn("foo");
+ assertNotNull("Grid should now have a column for property foo",
+ grid.getColumn("foo"));
+ assertNull("Grid should not have a column for property bar anymore",
+ grid.getColumn("bar"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddIncompatibleColumnProperty() {
+ grid.addColumn("bar");
+ grid.removeAllColumns();
+ grid.addColumn("bar", Integer.class);
+ }
+
+ @Test
+ public void testAddBooleanColumnProperty() {
+ grid.addColumn("foo", Boolean.class);
+ Property<?> property = container.getContainerProperty(
+ container.firstItemId(), "foo");
+ assertEquals(property.getType(), Boolean.class);
+ assertEquals(property.getValue(), null);
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
new file mode 100644
index 0000000000..5e96f4eeae
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+
+public class GridColumns {
+
+ private Grid grid;
+
+ private GridState state;
+
+ private Method getStateMethod;
+
+ private Field columnIdGeneratorField;
+
+ private KeyMapper<Object> columnIdMapper;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setup() throws Exception {
+ IndexedContainer ds = new IndexedContainer();
+ for (int c = 0; c < 10; c++) {
+ ds.addContainerProperty("column" + c, String.class, "");
+ }
+ ds.addContainerProperty("noSort", Object.class, null);
+ grid = new Grid(ds);
+
+ getStateMethod = Grid.class.getDeclaredMethod("getState");
+ getStateMethod.setAccessible(true);
+
+ state = (GridState) getStateMethod.invoke(grid);
+
+ columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
+ columnIdGeneratorField.setAccessible(true);
+
+ columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
+ }
+
+ @Test
+ public void testColumnGeneration() throws Exception {
+
+ for (Object propertyId : grid.getContainerDataSource()
+ .getContainerPropertyIds()) {
+
+ // All property ids should get a column
+ Column column = grid.getColumn(propertyId);
+ assertNotNull(column);
+
+ // Humanized property id should be the column header by default
+ assertEquals(
+ SharedUtil.camelCaseToHumanFriendly(propertyId.toString()),
+ grid.getDefaultHeaderRow().getCell(propertyId).getText());
+ }
+ }
+
+ @Test
+ public void testModifyingColumnProperties() throws Exception {
+
+ // Modify first column
+ Column column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ column.setHeaderCaption("CustomHeader");
+ assertEquals("CustomHeader", column.getHeaderCaption());
+ assertEquals(column.getHeaderCaption(), grid.getDefaultHeaderRow()
+ .getCell("column1").getText());
+
+ column.setWidth(100);
+ assertEquals(100, column.getWidth(), 0.49d);
+ assertEquals(column.getWidth(), getColumnState("column1").width, 0.49d);
+
+ try {
+ column.setWidth(-1);
+ fail("Setting width to -1 should throw exception");
+ } catch (IllegalArgumentException iae) {
+ // expected
+ }
+
+ assertEquals(100, column.getWidth(), 0.49d);
+ assertEquals(100, getColumnState("column1").width, 0.49d);
+ }
+
+ @Test
+ public void testRemovingColumnByRemovingPropertyFromContainer()
+ throws Exception {
+
+ Column column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ // Remove column
+ grid.getContainerDataSource().removeContainerProperty("column1");
+
+ try {
+ column.setHeaderCaption("asd");
+
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setWidth(123);
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ assertNull(grid.getColumn("column1"));
+ assertNull(getColumnState("column1"));
+ }
+
+ @Test
+ public void testAddingColumnByAddingPropertyToContainer() throws Exception {
+ grid.getContainerDataSource().addContainerProperty("columnX",
+ String.class, "");
+ Column column = grid.getColumn("columnX");
+ assertNotNull(column);
+ }
+
+ @Test
+ public void testHeaderVisiblility() throws Exception {
+
+ assertTrue(grid.isHeaderVisible());
+ assertTrue(state.header.visible);
+
+ grid.setHeaderVisible(false);
+ assertFalse(grid.isHeaderVisible());
+ assertFalse(state.header.visible);
+
+ grid.setHeaderVisible(true);
+ assertTrue(grid.isHeaderVisible());
+ assertTrue(state.header.visible);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+
+ assertTrue(grid.isFooterVisible());
+ assertTrue(state.footer.visible);
+
+ grid.setFooterVisible(false);
+ assertFalse(grid.isFooterVisible());
+ assertFalse(state.footer.visible);
+
+ grid.setFooterVisible(true);
+ assertTrue(grid.isFooterVisible());
+ assertTrue(state.footer.visible);
+ }
+
+ @Test
+ public void testFrozenColumnRemoveColumn() {
+ assertEquals("Grid should not start with a frozen column", 0,
+ grid.getFrozenColumnCount());
+
+ int containerSize = grid.getContainerDataSource()
+ .getContainerPropertyIds().size();
+ grid.setFrozenColumnCount(containerSize);
+
+ Object propertyId = grid.getContainerDataSource()
+ .getContainerPropertyIds().iterator().next();
+
+ grid.getContainerDataSource().removeContainerProperty(propertyId);
+ assertEquals(
+ "Frozen column count should update when removing last row",
+ containerSize - 1, grid.getFrozenColumnCount());
+ }
+
+ @Test
+ public void testReorderColumns() {
+ Set<?> containerProperties = new LinkedHashSet<Object>(grid
+ .getContainerDataSource().getContainerPropertyIds());
+ Object[] properties = new Object[] { "column3", "column2", "column6" };
+ grid.setColumnOrder(properties);
+
+ int i = 0;
+ // Test sorted columns are first in order
+ for (Object property : properties) {
+ containerProperties.remove(property);
+ assertEquals(columnIdMapper.key(property),
+ state.columnOrder.get(i++));
+ }
+
+ // Test remaining columns are in original order
+ for (Object property : containerProperties) {
+ assertEquals(columnIdMapper.key(property),
+ state.columnOrder.get(i++));
+ }
+
+ try {
+ grid.setColumnOrder("foo", "bar", "baz");
+ fail("Grid allowed sorting with non-existent properties");
+ } catch (IllegalArgumentException e) {
+ // All ok
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRemoveColumnThatDoesNotExist() {
+ grid.removeColumn("banana phone");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testSetNonSortableColumnSortable() {
+ Column noSortColumn = grid.getColumn("noSort");
+ assertFalse("Object property column should not be sortable.",
+ noSortColumn.isSortable());
+ noSortColumn.setSortable(true);
+ }
+
+ private GridColumnState getColumnState(Object propertyId) {
+ String columnId = columnIdMapper.key(propertyId);
+ for (GridColumnState columnState : state.columns) {
+ if (columnState.id.equals(columnId)) {
+ return columnState;
+ }
+ }
+ return null;
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java
new file mode 100644
index 0000000000..cdfd2328dc
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertFalse;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.AbstractInMemoryContainer;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+
+public class GridContainerNotSortableTest {
+
+ final AbstractInMemoryContainer<Object, Object, Item> notSortableDataSource = new AbstractInMemoryContainer<Object, Object, Item>() {
+
+ private Map<Object, Property<?>> properties = new LinkedHashMap<Object, Property<?>>();
+
+ {
+ properties.put("Foo", new Property<String>() {
+
+ @Override
+ public String getValue() {
+ return "foo";
+ }
+
+ @Override
+ public void setValue(String newValue) throws ReadOnlyException {
+ throw new ReadOnlyException();
+ }
+
+ @Override
+ public Class<? extends String> getType() {
+ return String.class;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ throw new UnsupportedOperationException();
+ }
+ });
+ }
+
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return properties.keySet();
+ }
+
+ @Override
+ public Property getContainerProperty(Object itemId, Object propertyId) {
+ return properties.get(propertyId);
+ }
+
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return properties.get(propertyId).getType();
+ }
+
+ @Override
+ protected Item getUnfilteredItem(Object itemId) {
+ return null;
+ }
+ };
+
+ @Test
+ public void testGridWithNotSortableContainer() {
+ new Grid(notSortableDataSource);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testNotSortableGridSetColumnSortable() {
+ Grid grid = new Grid();
+ grid.setContainerDataSource(notSortableDataSource);
+ Column column = grid.getColumn("Foo");
+ assertFalse("Column should not be sortable initially.",
+ column.isSortable());
+ column.setSortable(true);
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridEditorTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridEditorTest.java
new file mode 100644
index 0000000000..b247876d5d
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridEditorTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Method;
+
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.MockVaadinSession;
+import com.vaadin.server.VaadinService;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.TextField;
+
+public class GridEditorTest {
+
+ private static final Object PROPERTY_NAME = "name";
+ private static final Object PROPERTY_AGE = "age";
+ private static final String DEFAULT_NAME = "Some Valid Name";
+ private static final Integer DEFAULT_AGE = 25;
+ private static final Object ITEM_ID = new Object();
+
+ // Explicit field for the test session to save it from GC
+ private VaadinSession session;
+
+ private final Grid grid = new Grid();
+ private Method doEditMethod;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setup() throws SecurityException, NoSuchMethodException {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty(PROPERTY_NAME, String.class, "[name]");
+ container.addContainerProperty(PROPERTY_AGE, Integer.class,
+ Integer.valueOf(-1));
+
+ Item item = container.addItem(ITEM_ID);
+ item.getItemProperty(PROPERTY_NAME).setValue(DEFAULT_NAME);
+ item.getItemProperty(PROPERTY_AGE).setValue(DEFAULT_AGE);
+ grid.setContainerDataSource(container);
+
+ // VaadinSession needed for ConverterFactory
+ VaadinService mockService = EasyMock
+ .createNiceMock(VaadinService.class);
+ session = new MockVaadinSession(mockService);
+ VaadinSession.setCurrent(session);
+ session.lock();
+
+ // Access to method for actual editing.
+ doEditMethod = Grid.class.getDeclaredMethod("doEditItem");
+ doEditMethod.setAccessible(true);
+ }
+
+ @After
+ public void tearDown() {
+ session.unlock();
+ session = null;
+ VaadinSession.setCurrent(null);
+ }
+
+ @Test
+ public void testInitAssumptions() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ assertNull(grid.getEditedItemId());
+ assertNotNull(grid.getEditorFieldGroup());
+ }
+
+ @Test
+ public void testSetEnabled() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ grid.setEditorEnabled(true);
+ assertTrue(grid.isEditorEnabled());
+ }
+
+ @Test
+ public void testSetDisabled() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ grid.setEditorEnabled(true);
+ grid.setEditorEnabled(false);
+ assertFalse(grid.isEditorEnabled());
+ }
+
+ @Test
+ public void testSetReEnabled() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ grid.setEditorEnabled(true);
+ grid.setEditorEnabled(false);
+ grid.setEditorEnabled(true);
+ assertTrue(grid.isEditorEnabled());
+ }
+
+ @Test
+ public void testDetached() throws Exception {
+ FieldGroup oldFieldGroup = grid.getEditorFieldGroup();
+ grid.removeAllColumns();
+ grid.setContainerDataSource(new IndexedContainer());
+ assertFalse(oldFieldGroup == grid.getEditorFieldGroup());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testDisabledEditItem() throws Exception {
+ grid.editItem(ITEM_ID);
+ }
+
+ @Test
+ public void testEditItem() throws Exception {
+ startEdit();
+ assertEquals(ITEM_ID, grid.getEditedItemId());
+ assertEquals(getEditedItem(), grid.getEditorFieldGroup()
+ .getItemDataSource());
+
+ assertEquals(DEFAULT_NAME, grid.getEditorField(PROPERTY_NAME)
+ .getValue());
+ assertEquals(String.valueOf(DEFAULT_AGE),
+ grid.getEditorField(PROPERTY_AGE).getValue());
+ }
+
+ @Test
+ public void testSaveEditor() throws Exception {
+ startEdit();
+ TextField field = (TextField) grid.getEditorField(PROPERTY_NAME);
+
+ field.setValue("New Name");
+ assertEquals(DEFAULT_NAME, field.getPropertyDataSource().getValue());
+
+ grid.saveEditor();
+ assertTrue(grid.isEditorActive());
+ assertFalse(field.isModified());
+ assertEquals("New Name", field.getValue());
+ assertEquals("New Name", getEditedProperty(PROPERTY_NAME).getValue());
+ }
+
+ @Test
+ public void testSaveEditorCommitFail() throws Exception {
+ startEdit();
+
+ ((TextField) grid.getEditorField(PROPERTY_AGE)).setValue("Invalid");
+ try {
+ // Manual fail instead of @Test(expected=...) to check it is
+ // saveEditor that fails and not setValue
+ grid.saveEditor();
+ Assert.fail("CommitException expected when saving an invalid field value");
+ } catch (CommitException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testCancelEditor() throws Exception {
+ startEdit();
+ TextField field = (TextField) grid.getEditorField(PROPERTY_NAME);
+ field.setValue("New Name");
+
+ grid.cancelEditor();
+ assertFalse(grid.isEditorActive());
+ assertNull(grid.getEditedItemId());
+ assertFalse(field.isModified());
+ assertEquals(DEFAULT_NAME, field.getValue());
+ assertEquals(DEFAULT_NAME, field.getPropertyDataSource().getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNonexistentEditItem() throws Exception {
+ grid.setEditorEnabled(true);
+ grid.editItem(new Object());
+ }
+
+ @Test
+ public void testGetField() throws Exception {
+ startEdit();
+
+ assertNotNull(grid.getEditorField(PROPERTY_NAME));
+ }
+
+ @Test
+ public void testGetFieldWithoutItem() throws Exception {
+ grid.setEditorEnabled(true);
+ assertNotNull(grid.getEditorField(PROPERTY_NAME));
+ }
+
+ @Test
+ public void testCustomBinding() {
+ TextField textField = new TextField();
+ grid.setEditorField(PROPERTY_NAME, textField);
+
+ startEdit();
+
+ assertSame(textField, grid.getEditorField(PROPERTY_NAME));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testDisableWhileEditing() {
+ startEdit();
+ grid.setEditorEnabled(false);
+ }
+
+ @Test
+ public void testFieldIsNotReadonly() {
+ startEdit();
+
+ Field<?> field = grid.getEditorField(PROPERTY_NAME);
+ assertFalse(field.isReadOnly());
+ }
+
+ @Test
+ public void testFieldIsReadonlyWhenFieldGroupIsReadonly() {
+ startEdit();
+
+ grid.getEditorFieldGroup().setReadOnly(true);
+ Field<?> field = grid.getEditorField(PROPERTY_NAME);
+ assertTrue(field.isReadOnly());
+ }
+
+ @Test
+ public void testColumnRemoved() {
+ Field<?> field = grid.getEditorField(PROPERTY_NAME);
+
+ assertSame("field should be attached to ", grid, field.getParent());
+
+ grid.removeColumn(PROPERTY_NAME);
+
+ assertNull("field should be detached from ", field.getParent());
+ }
+
+ @Test
+ public void testSetFieldAgain() {
+ TextField field = new TextField();
+ grid.setEditorField(PROPERTY_NAME, field);
+
+ field = new TextField();
+ grid.setEditorField(PROPERTY_NAME, field);
+
+ assertSame("new field should be used.", field,
+ grid.getEditorField(PROPERTY_NAME));
+ }
+
+ private void startEdit() {
+ grid.setEditorEnabled(true);
+ grid.editItem(ITEM_ID);
+ // Simulate succesful client response to actually start the editing.
+ try {
+ doEditMethod.invoke(grid);
+ } catch (Exception e) {
+ Assert.fail("Editing item " + ITEM_ID + " failed. Cause: "
+ + e.getCause().toString());
+ }
+ }
+
+ private Item getEditedItem() {
+ assertNotNull(grid.getEditedItemId());
+ return grid.getContainerDataSource().getItem(grid.getEditedItemId());
+ }
+
+ private Property<?> getEditedProperty(Object propertyId) {
+ return getEditedItem().getItemProperty(PROPERTY_NAME);
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java
new file mode 100644
index 0000000000..7f09677b50
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.Grid.SelectionModel;
+
+public class GridSelection {
+
+ private static class MockSelectionChangeListener implements
+ SelectionListener {
+ private SelectionEvent event;
+
+ @Override
+ public void select(final SelectionEvent event) {
+ this.event = event;
+ }
+
+ public Collection<?> getAdded() {
+ return event.getAdded();
+ }
+
+ public Collection<?> getRemoved() {
+ return event.getRemoved();
+ }
+
+ public void clearEvent() {
+ /*
+ * This method is not strictly needed as the event will simply be
+ * overridden, but it's good practice, and makes the code more
+ * obvious.
+ */
+ event = null;
+ }
+
+ public boolean eventHasHappened() {
+ return event != null;
+ }
+ }
+
+ private Grid grid;
+ private MockSelectionChangeListener mockListener;
+
+ private final Object itemId1Present = "itemId1Present";
+ private final Object itemId2Present = "itemId2Present";
+
+ private final Object itemId1NotPresent = "itemId1NotPresent";
+ private final Object itemId2NotPresent = "itemId2NotPresent";
+
+ @Before
+ public void setup() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ for (int i = 2; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ assertEquals("init size", 10, container.size());
+ assertTrue("itemId1Present", container.containsId(itemId1Present));
+ assertTrue("itemId2Present", container.containsId(itemId2Present));
+ assertFalse("itemId1NotPresent",
+ container.containsId(itemId1NotPresent));
+ assertFalse("itemId2NotPresent",
+ container.containsId(itemId2NotPresent));
+
+ grid = new Grid(container);
+
+ mockListener = new MockSelectionChangeListener();
+ grid.addSelectionListener(mockListener);
+
+ assertFalse("eventHasHappened", mockListener.eventHasHappened());
+ }
+
+ @Test
+ public void defaultSelectionModeIsSingle() {
+ assertTrue(grid.getSelectionModel() instanceof SelectionModel.Single);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getSelectedRowThrowsExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.getSelectedRow();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getSelectedRowThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.getSelectedRow();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void selectThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.select(itemId1Present);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void deselectRowThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.deselect(itemId1Present);
+ }
+
+ @Test
+ public void selectionModeMapsToMulti() {
+ assertTrue(grid.setSelectionMode(SelectionMode.MULTI) instanceof SelectionModel.Multi);
+ }
+
+ @Test
+ public void selectionModeMapsToSingle() {
+ assertTrue(grid.setSelectionMode(SelectionMode.SINGLE) instanceof SelectionModel.Single);
+ }
+
+ @Test
+ public void selectionModeMapsToNone() {
+ assertTrue(grid.setSelectionMode(SelectionMode.NONE) instanceof SelectionModel.None);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectionModeNullThrowsException() {
+ grid.setSelectionMode(null);
+ }
+
+ @Test
+ public void noSelectModel_isSelected() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ assertFalse("itemId1Present", grid.isSelected(itemId1Present));
+ assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void noSelectModel_getSelectedRow() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.getSelectedRow();
+ }
+
+ @Test
+ public void noSelectModel_getSelectedRows() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ assertTrue(grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void selectionCallsListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ selectionCallsListener();
+ }
+
+ @Test
+ public void selectionCallsListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ selectionCallsListener();
+ }
+
+ private void selectionCallsListener() {
+ grid.select(itemId1Present);
+ assertEquals("added size", 1, mockListener.getAdded().size());
+ assertEquals("added item", itemId1Present, mockListener.getAdded()
+ .iterator().next());
+ assertEquals("removed size", 0, mockListener.getRemoved().size());
+ }
+
+ @Test
+ public void deselectionCallsListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ deselectionCallsListener();
+ }
+
+ @Test
+ public void deselectionCallsListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ deselectionCallsListener();
+ }
+
+ private void deselectionCallsListener() {
+ grid.select(itemId1Present);
+ mockListener.clearEvent();
+
+ grid.deselect(itemId1Present);
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("removed item", itemId1Present, mockListener.getRemoved()
+ .iterator().next());
+ assertEquals("removed size", 0, mockListener.getAdded().size());
+ }
+
+ @Test
+ public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ deselectPresentButNotSelectedItemIdShouldntFireListener();
+ }
+
+ @Test
+ public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ deselectPresentButNotSelectedItemIdShouldntFireListener();
+ }
+
+ private void deselectPresentButNotSelectedItemIdShouldntFireListener() {
+ grid.deselect(itemId1Present);
+ assertFalse(mockListener.eventHasHappened());
+ }
+
+ @Test
+ public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.deselect(itemId1NotPresent);
+ }
+
+ @Test
+ public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.deselect(itemId1NotPresent);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectNotPresentItemIdShouldThrowExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.select(itemId1NotPresent);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectNotPresentItemIdShouldThrowExceptionSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.select(itemId1NotPresent);
+ }
+
+ @Test
+ public void selectAllMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ assertEquals("added size", 10, mockListener.getAdded().size());
+ assertEquals("removed size", 0, mockListener.getRemoved().size());
+ assertTrue("itemId1Present",
+ mockListener.getAdded().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getAdded().contains(itemId2Present));
+ }
+
+ @Test
+ public void deselectAllMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ mockListener.clearEvent();
+
+ select.deselectAll();
+ assertEquals("removed size", 10, mockListener.getRemoved().size());
+ assertEquals("added size", 0, mockListener.getAdded().size());
+ assertTrue("itemId1Present",
+ mockListener.getRemoved().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getRemoved().contains(itemId2Present));
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void reselectionDeselectsPreviousSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.select(itemId1Present);
+ mockListener.clearEvent();
+
+ grid.select(itemId2Present);
+ assertEquals("added size", 1, mockListener.getAdded().size());
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("added item", itemId2Present, mockListener.getAdded()
+ .iterator().next());
+ assertEquals("removed item", itemId1Present, mockListener.getRemoved()
+ .iterator().next());
+ assertEquals("selectedRows is correct", itemId2Present,
+ grid.getSelectedRow());
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java
new file mode 100644
index 0000000000..4031886e7a
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.Grid;
+
+public class GridStaticSectionTest extends Grid {
+
+ private Indexed dataSource = new IndexedContainer();
+
+ @Before
+ public void setUp() {
+ dataSource.addContainerProperty("firstName", String.class, "");
+ dataSource.addContainerProperty("lastName", String.class, "");
+ dataSource.addContainerProperty("streetAddress", String.class, "");
+ dataSource.addContainerProperty("zipCode", Integer.class, null);
+ setContainerDataSource(dataSource);
+ }
+
+ @Test
+ public void testAddAndRemoveHeaders() {
+ assertEquals(1, getHeaderRowCount());
+ prependHeaderRow();
+ assertEquals(2, getHeaderRowCount());
+ removeHeaderRow(0);
+ assertEquals(1, getHeaderRowCount());
+ removeHeaderRow(0);
+ assertEquals(0, getHeaderRowCount());
+ assertEquals(null, getDefaultHeaderRow());
+ HeaderRow row = appendHeaderRow();
+ assertEquals(1, getHeaderRowCount());
+ assertEquals(null, getDefaultHeaderRow());
+ setDefaultHeaderRow(row);
+ assertEquals(row, getDefaultHeaderRow());
+ }
+
+ @Test
+ public void testAddAndRemoveFooters() {
+ // By default there are no footer rows
+ assertEquals(0, getFooterRowCount());
+ FooterRow row = appendFooterRow();
+
+ assertEquals(1, getFooterRowCount());
+ prependFooterRow();
+ assertEquals(2, getFooterRowCount());
+ assertEquals(row, getFooterRow(1));
+ removeFooterRow(0);
+ assertEquals(1, getFooterRowCount());
+ removeFooterRow(0);
+ assertEquals(0, getFooterRowCount());
+ }
+
+ @Test
+ public void testUnusedPropertyNotInCells() {
+ removeColumn("firstName");
+ assertNull("firstName cell was not removed from existing row",
+ getDefaultHeaderRow().getCell("firstName"));
+ HeaderRow newRow = appendHeaderRow();
+ assertNull("firstName cell was created when it should not.",
+ newRow.getCell("firstName"));
+ addColumn("firstName");
+ assertNotNull(
+ "firstName cell was not created for default row when added again",
+ getDefaultHeaderRow().getCell("firstName"));
+ assertNotNull(
+ "firstName cell was not created for new row when added again",
+ newRow.getCell("firstName"));
+
+ }
+
+ @Test
+ public void testJoinHeaderCells() {
+ HeaderRow mergeRow = prependHeaderRow();
+ mergeRow.join("firstName", "lastName").setText("Name");
+ mergeRow.join(mergeRow.getCell("streetAddress"),
+ mergeRow.getCell("zipCode"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testJoinHeaderCellsIncorrectly() throws Throwable {
+ HeaderRow mergeRow = prependHeaderRow();
+ mergeRow.join("firstName", "zipCode").setText("Name");
+ sanityCheck();
+ }
+
+ @Test
+ public void testJoinAllFooterCells() {
+ FooterRow mergeRow = prependFooterRow();
+ mergeRow.join(dataSource.getContainerPropertyIds().toArray()).setText(
+ "All the stuff.");
+ }
+
+ private void sanityCheck() throws Throwable {
+ Method sanityCheckHeader;
+ try {
+ sanityCheckHeader = Grid.Header.class
+ .getDeclaredMethod("sanityCheck");
+ sanityCheckHeader.setAccessible(true);
+ Method sanityCheckFooter = Grid.Footer.class
+ .getDeclaredMethod("sanityCheck");
+ sanityCheckFooter.setAccessible(true);
+ sanityCheckHeader.invoke(getHeader());
+ sanityCheckFooter.invoke(getFooter());
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java
new file mode 100644
index 0000000000..9b327a2f22
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.MultiSelectionModel;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class MultiSelectionModelTest {
+
+ private Object itemId1Present = "itemId1Present";
+ private Object itemId2Present = "itemId2Present";
+ private Object itemId3Present = "itemId3Present";
+
+ private Object itemIdNotPresent = "itemIdNotPresent";
+ private Container.Indexed dataSource;
+ private MultiSelectionModel model;
+ private Grid grid;
+
+ private boolean expectingEvent = false;
+ private boolean expectingDeselectEvent;
+ private List<Object> select = new ArrayList<Object>();
+ private List<Object> deselect = new ArrayList<Object>();
+
+ @Before
+ public void setUp() {
+ dataSource = createDataSource();
+ grid = new Grid(dataSource);
+ grid.setSelectionMode(SelectionMode.MULTI);
+ model = (MultiSelectionModel) grid.getSelectionModel();
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertFalse("Some expected select event did not happen.",
+ expectingEvent);
+ Assert.assertFalse("Some expected deselect event did not happen.",
+ expectingDeselectEvent);
+ }
+
+ private IndexedContainer createDataSource() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ container.addItem(itemId3Present);
+ for (int i = 3; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ return container;
+ }
+
+ @Test
+ public void testSelectAndDeselectRow() throws Throwable {
+ try {
+ expectSelectEvent(itemId1Present);
+ model.select(itemId1Present);
+ expectDeselectEvent(itemId1Present);
+ model.deselect(itemId1Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+
+ verifyCurrentSelection();
+ }
+
+ @Test
+ public void testAddSelection() throws Throwable {
+ try {
+ expectSelectEvent(itemId1Present);
+ model.select(itemId1Present);
+ expectSelectEvent(itemId2Present);
+ model.select(itemId2Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+
+ verifyCurrentSelection(itemId1Present, itemId2Present);
+ }
+
+ @Test
+ public void testSettingSelection() throws Throwable {
+ try {
+ expectSelectEvent(itemId2Present, itemId1Present);
+ model.setSelected(Arrays.asList(new Object[] { itemId1Present,
+ itemId2Present }));
+ verifyCurrentSelection(itemId1Present, itemId2Present);
+
+ expectDeselectEvent(itemId1Present);
+ expectSelectEvent(itemId3Present);
+ model.setSelected(Arrays.asList(new Object[] { itemId3Present,
+ itemId2Present }));
+ verifyCurrentSelection(itemId3Present, itemId2Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ private void expectSelectEvent(Object... selectArray) {
+ select = Arrays.asList(selectArray);
+ addListener();
+ }
+
+ private void expectDeselectEvent(Object... deselectArray) {
+ deselect = Arrays.asList(deselectArray);
+ addListener();
+ }
+
+ private void addListener() {
+ if (expectingEvent) {
+ return;
+ }
+
+ expectingEvent = true;
+ grid.addSelectionListener(new SelectionListener() {
+
+ @Override
+ public void select(SelectionEvent event) {
+ Assert.assertTrue("Selection did not contain expected items",
+ event.getAdded().containsAll(select));
+ Assert.assertTrue("Selection contained unexpected items",
+ select.containsAll(event.getAdded()));
+ select = new ArrayList<Object>();
+
+ Assert.assertTrue("Deselection did not contain expected items",
+ event.getRemoved().containsAll(deselect));
+ Assert.assertTrue("Deselection contained unexpected items",
+ deselect.containsAll(event.getRemoved()));
+ deselect = new ArrayList<Object>();
+
+ grid.removeSelectionListener(this);
+ expectingEvent = false;
+ }
+ });
+ }
+
+ private void verifyCurrentSelection(Object... selection) {
+ final List<Object> selected = Arrays.asList(selection);
+ if (model.getSelectedRows().containsAll(selected)
+ && selected.containsAll(model.getSelectedRows())) {
+ return;
+ }
+ Assert.fail("Not all items were correctly selected");
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java
new file mode 100644
index 0000000000..c217efb935
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.Grid.SingleSelectionModel;
+
+public class SingleSelectionModelTest {
+
+ private Object itemId1Present = "itemId1Present";
+ private Object itemId2Present = "itemId2Present";
+
+ private Object itemIdNotPresent = "itemIdNotPresent";
+ private Container.Indexed dataSource;
+ private SingleSelectionModel model;
+ private Grid grid;
+
+ private boolean expectingEvent = false;
+
+ @Before
+ public void setUp() {
+ dataSource = createDataSource();
+ grid = new Grid(dataSource);
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ model = (SingleSelectionModel) grid.getSelectionModel();
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertFalse("Some expected event did not happen.",
+ expectingEvent);
+ }
+
+ private IndexedContainer createDataSource() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ for (int i = 2; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ return container;
+ }
+
+ @Test
+ public void testSelectAndDeselctRow() throws Throwable {
+ try {
+ expectEvent(itemId1Present, null);
+ model.select(itemId1Present);
+ expectEvent(null, itemId1Present);
+ model.select(null);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void testSelectAndChangeSelectedRow() throws Throwable {
+ try {
+ expectEvent(itemId1Present, null);
+ model.select(itemId1Present);
+ expectEvent(itemId2Present, itemId1Present);
+ model.select(itemId2Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void testRemovingSelectedRowAndThenDeselecting() throws Throwable {
+ try {
+ expectEvent(itemId2Present, null);
+ model.select(itemId2Present);
+ dataSource.removeItem(itemId2Present);
+ expectEvent(null, itemId2Present);
+ model.select(null);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void testSelectAndReSelectRow() throws Throwable {
+ try {
+ expectEvent(itemId1Present, null);
+ model.select(itemId1Present);
+ expectEvent(null, null);
+ // This is no-op. Nothing should happen.
+ model.select(itemId1Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ Assert.assertTrue("Should still wait for event", expectingEvent);
+ expectingEvent = false;
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSelectNonExistentRow() {
+ model.select(itemIdNotPresent);
+ }
+
+ private void expectEvent(final Object selected, final Object deselected) {
+ expectingEvent = true;
+ grid.addSelectionListener(new SelectionListener() {
+
+ @Override
+ public void select(SelectionEvent event) {
+ if (selected != null) {
+ Assert.assertTrue(
+ "Selection did not contain expected item", event
+ .getAdded().contains(selected));
+ } else {
+ Assert.assertTrue("Unexpected selection", event.getAdded()
+ .isEmpty());
+ }
+
+ if (deselected != null) {
+ Assert.assertTrue(
+ "DeSelection did not contain expected item", event
+ .getRemoved().contains(deselected));
+ } else {
+ Assert.assertTrue("Unexpected selection", event
+ .getRemoved().isEmpty());
+ }
+
+ grid.removeSelectionListener(this);
+ expectingEvent = false;
+ }
+ });
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java
new file mode 100644
index 0000000000..2a682df2e5
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.component.grid.sort;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.ui.Grid;
+
+public class SortTest {
+
+ class DummySortingIndexedContainer extends IndexedContainer {
+
+ private Object[] expectedProperties;
+ private boolean[] expectedAscending;
+ private boolean sorted = true;
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ Assert.assertEquals(
+ "Different amount of expected and actual properties,",
+ expectedProperties.length, propertyId.length);
+ Assert.assertEquals(
+ "Different amount of expected and actual directions",
+ expectedAscending.length, ascending.length);
+ for (int i = 0; i < propertyId.length; ++i) {
+ Assert.assertEquals("Sorting properties differ",
+ expectedProperties[i], propertyId[i]);
+ Assert.assertEquals("Sorting directions differ",
+ expectedAscending[i], ascending[i]);
+ }
+ sorted = true;
+ }
+
+ public void expectedSort(Object[] properties, SortDirection[] directions) {
+ assert directions.length == properties.length : "Array dimensions differ";
+ expectedProperties = properties;
+ expectedAscending = new boolean[directions.length];
+ for (int i = 0; i < directions.length; ++i) {
+ expectedAscending[i] = (directions[i] == SortDirection.ASCENDING);
+ }
+ sorted = false;
+ }
+
+ public boolean isSorted() {
+ return sorted;
+ }
+ }
+
+ class RegisteringSortChangeListener implements SortListener {
+ private List<SortOrder> order;
+
+ @Override
+ public void sort(SortEvent event) {
+ assert order == null : "The same listener was notified multipe times without checking";
+
+ order = event.getSortOrder();
+ }
+
+ public void assertEventFired(SortOrder... expectedOrder) {
+ Assert.assertEquals(Arrays.asList(expectedOrder), order);
+
+ // Reset for nest test
+ order = null;
+ }
+
+ }
+
+ private DummySortingIndexedContainer container;
+ private RegisteringSortChangeListener listener;
+ private Grid grid;
+
+ @Before
+ public void setUp() {
+ container = createContainer();
+ container.expectedSort(new Object[] {}, new SortDirection[] {});
+
+ listener = new RegisteringSortChangeListener();
+
+ grid = new Grid(container);
+ grid.addSortListener(listener);
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertTrue("Container was not sorted after the test.",
+ container.isSorted());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidSortDirection() {
+ Sort.by("foo", null);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testSortOneColumnMultipleTimes() {
+ Sort.by("foo").then("bar").then("foo");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSortingByUnexistingProperty() {
+ grid.sort("foobar");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSortingByUnsortableProperty() {
+ container.addContainerProperty("foobar", Object.class, null);
+ grid.sort("foobar");
+ }
+
+ @Test
+ public void testGridDirectSortAscending() {
+ container.expectedSort(new Object[] { "foo" },
+ new SortDirection[] { SortDirection.ASCENDING });
+ grid.sort("foo");
+
+ listener.assertEventFired(new SortOrder("foo", SortDirection.ASCENDING));
+ }
+
+ @Test
+ public void testGridDirectSortDescending() {
+ container.expectedSort(new Object[] { "foo" },
+ new SortDirection[] { SortDirection.DESCENDING });
+ grid.sort("foo", SortDirection.DESCENDING);
+
+ listener.assertEventFired(new SortOrder("foo", SortDirection.DESCENDING));
+ }
+
+ @Test
+ public void testGridSortBy() {
+ container.expectedSort(new Object[] { "foo", "bar", "baz" },
+ new SortDirection[] { SortDirection.ASCENDING,
+ SortDirection.ASCENDING, SortDirection.DESCENDING });
+ grid.sort(Sort.by("foo").then("bar")
+ .then("baz", SortDirection.DESCENDING));
+
+ listener.assertEventFired(
+ new SortOrder("foo", SortDirection.ASCENDING), new SortOrder(
+ "bar", SortDirection.ASCENDING), new SortOrder("baz",
+ SortDirection.DESCENDING));
+
+ }
+
+ @Test
+ public void testChangeContainerAfterSorting() {
+ class Person {
+ }
+
+ container.expectedSort(new Object[] { "foo", "bar", "baz" },
+ new SortDirection[] { SortDirection.ASCENDING,
+ SortDirection.ASCENDING, SortDirection.DESCENDING });
+ grid.sort(Sort.by("foo").then("bar")
+ .then("baz", SortDirection.DESCENDING));
+
+ listener.assertEventFired(
+ new SortOrder("foo", SortDirection.ASCENDING), new SortOrder(
+ "bar", SortDirection.ASCENDING), new SortOrder("baz",
+ SortDirection.DESCENDING));
+
+ container = new DummySortingIndexedContainer();
+ container.addContainerProperty("foo", Person.class, null);
+ container.addContainerProperty("baz", String.class, "");
+ container.addContainerProperty("bar", Person.class, null);
+ container.expectedSort(new Object[] { "baz" },
+ new SortDirection[] { SortDirection.DESCENDING });
+ grid.setContainerDataSource(container);
+
+ listener.assertEventFired(new SortOrder("baz", SortDirection.DESCENDING));
+
+ }
+
+ private DummySortingIndexedContainer createContainer() {
+ DummySortingIndexedContainer container = new DummySortingIndexedContainer();
+ container.addContainerProperty("foo", Integer.class, 0);
+ container.addContainerProperty("bar", Integer.class, 0);
+ container.addContainerProperty("baz", Integer.class, 0);
+ return container;
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java b/server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java
new file mode 100644
index 0000000000..08f045277d
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.renderer;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.server.ClassResource;
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.FileResource;
+import com.vaadin.server.FontAwesome;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.renderer.ImageRenderer;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+public class ImageRendererTest {
+
+ private ImageRenderer renderer;
+
+ @Before
+ public void setUp() {
+ UI mockUI = EasyMock.createNiceMock(UI.class);
+ EasyMock.replay(mockUI);
+
+ Grid grid = new Grid();
+ grid.setParent(mockUI);
+
+ renderer = new ImageRenderer();
+ renderer.setParent(grid);
+ }
+
+ @Test
+ public void testThemeResource() {
+ JsonValue v = renderer.encode(new ThemeResource("foo.png"));
+ assertEquals("theme://foo.png", getUrl(v));
+ }
+
+ @Test
+ public void testExternalResource() {
+ JsonValue v = renderer.encode(new ExternalResource(
+ "http://example.com/foo.png"));
+ assertEquals("http://example.com/foo.png", getUrl(v));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFileResource() {
+ renderer.encode(new FileResource(new File("/tmp/foo.png")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testClassResource() {
+ renderer.encode(new ClassResource("img/foo.png"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFontIcon() {
+ renderer.encode(FontAwesome.AMBULANCE);
+ }
+
+ private String getUrl(JsonValue v) {
+ return ((JsonObject) v).get("uRL").asString();
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java b/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java
new file mode 100644
index 0000000000..767c72f5d9
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.server.renderer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Locale;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.StringToIntegerConverter;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.tests.util.AlwaysLockedVaadinSession;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.renderer.ObjectRenderer;
+import com.vaadin.ui.renderer.TextRenderer;
+
+import elemental.json.JsonValue;
+
+public class RendererTest {
+
+ private static class TestBean {
+ int i = 42;
+
+ @Override
+ public String toString() {
+ return "TestBean [" + i + "]";
+ }
+ }
+
+ private static class ExtendedBean extends TestBean {
+ float f = 3.14f;
+ }
+
+ private static class TestRenderer extends TextRenderer {
+ @Override
+ public JsonValue encode(String value) {
+ return super.encode("renderer(" + value + ")");
+ }
+ }
+
+ private static class TestConverter implements Converter<String, TestBean> {
+
+ @Override
+ public TestBean convertToModel(String value,
+ Class<? extends TestBean> targetType, Locale locale)
+ throws ConversionException {
+ return null;
+ }
+
+ @Override
+ public String convertToPresentation(TestBean value,
+ Class<? extends String> targetType, Locale locale)
+ throws ConversionException {
+ if (value instanceof ExtendedBean) {
+ return "ExtendedBean(" + value.i + ", "
+ + ((ExtendedBean) value).f + ")";
+ } else {
+ return "TestBean(" + value.i + ")";
+ }
+ }
+
+ @Override
+ public Class<TestBean> getModelType() {
+ return TestBean.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+ }
+
+ private Grid grid;
+
+ private Column foo;
+ private Column bar;
+ private Column baz;
+ private Column bah;
+
+ @Before
+ public void setUp() {
+ VaadinSession.setCurrent(new AlwaysLockedVaadinSession(null));
+
+ IndexedContainer c = new IndexedContainer();
+
+ c.addContainerProperty("foo", Integer.class, 0);
+ c.addContainerProperty("bar", String.class, "");
+ c.addContainerProperty("baz", TestBean.class, null);
+ c.addContainerProperty("bah", ExtendedBean.class, null);
+
+ Object id = c.addItem();
+ Item item = c.getItem(id);
+ item.getItemProperty("foo").setValue(123);
+ item.getItemProperty("bar").setValue("321");
+ item.getItemProperty("baz").setValue(new TestBean());
+ item.getItemProperty("bah").setValue(new ExtendedBean());
+
+ UI ui = EasyMock.createNiceMock(UI.class);
+ ConnectorTracker ct = EasyMock.createNiceMock(ConnectorTracker.class);
+ EasyMock.expect(ui.getConnectorTracker()).andReturn(ct).anyTimes();
+ EasyMock.replay(ui, ct);
+
+ grid = new Grid(c);
+ grid.setParent(ui);
+
+ foo = grid.getColumn("foo");
+ bar = grid.getColumn("bar");
+ baz = grid.getColumn("baz");
+ bah = grid.getColumn("bah");
+ }
+
+ @Test
+ public void testDefaultRendererAndConverter() throws Exception {
+ assertTrue("Foo default renderer should be a type of ObjectRenderer",
+ foo.getRenderer() instanceof ObjectRenderer);
+
+ assertTrue("Bar default renderer should be a type of ObjectRenderer",
+ bar.getRenderer() instanceof ObjectRenderer);
+ // String->String; converter not needed
+ assertNull(bar.getConverter());
+
+ assertTrue("Baz default renderer should be a type of ObjectRenderer",
+ baz.getRenderer() instanceof ObjectRenderer);
+ // MyBean->String; converter not found
+ assertNull(baz.getConverter());
+ }
+
+ @Test
+ public void testFindCompatibleConverter() throws Exception {
+ foo.setRenderer(renderer());
+ assertSame(StringToIntegerConverter.class, foo.getConverter()
+ .getClass());
+
+ bar.setRenderer(renderer());
+ assertNull(bar.getConverter());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCannotFindConverter() {
+ baz.setRenderer(renderer());
+ }
+
+ @Test
+ public void testExplicitConverter() throws Exception {
+ baz.setRenderer(renderer(), converter());
+ bah.setRenderer(renderer(), converter());
+ }
+
+ @Test
+ public void testEncoding() throws Exception {
+ /*
+ * For some strange reason, this test seems to fail locally, but not on
+ * TeamCity.
+ */
+
+ assertEquals("42", render(foo, 42).asString());
+ foo.setRenderer(renderer());
+ assertEquals("renderer(42)", render(foo, 42).asString());
+
+ assertEquals("2.72", render(bar, "2.72").asString());
+ bar.setRenderer(new TestRenderer());
+ assertEquals("renderer(2.72)", render(bar, "2.72").asString());
+ }
+
+ @Test
+ public void testEncodingWithoutConverter() throws Exception {
+ assertEquals("TestBean [42]", render(baz, new TestBean()).asString());
+ }
+
+ @Test
+ public void testBeanEncoding() throws Exception {
+ baz.setRenderer(renderer(), converter());
+ bah.setRenderer(renderer(), converter());
+
+ assertEquals("renderer(TestBean(42))", render(baz, new TestBean())
+ .asString());
+ assertEquals("renderer(ExtendedBean(42, 3.14))",
+ render(baz, new ExtendedBean()).asString());
+
+ assertEquals("renderer(ExtendedBean(42, 3.14))",
+ render(bah, new ExtendedBean()).asString());
+ }
+
+ private TestConverter converter() {
+ return new TestConverter();
+ }
+
+ private TestRenderer renderer() {
+ return new TestRenderer();
+ }
+
+ private JsonValue render(Column column, Object value) {
+ return RpcDataProviderExtension.encodeValue(value,
+ column.getRenderer(), column.getConverter(), grid.getLocale());
+ }
+}
diff --git a/shared/build.xml b/shared/build.xml
index 1e7e788be5..a9cd0b9803 100644
--- a/shared/build.xml
+++ b/shared/build.xml
@@ -1,8 +1,10 @@
<?xml version="1.0"?>
-<project name="vaadin-shared" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-shared" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
- Compiles build helpers used when building other modules.
+ Compiles build helpers used when building other
+ modules.
</description>
<include file="../common.xml" as="common" />
<include file="../build.xml" as="vaadin" />
@@ -14,11 +16,14 @@
<property name="result.dir" location="result" />
<property name="src.filtered" location="${result.dir}/filtered-src" />
<property name="src" location="${src.filtered}" />
- <path id="classpath.compile.custom" />
+ <path id="classpath.compile.custom">
+ <fileset file="${gwt.elemental.jar}" />
+ </path>
<path id="classpath.test.custom" />
<target name="jar">
- <property name="shared.osgi.import" value="org.json;version=&quot;0.0.20080701&quot;, com.google.gwt.thirdparty.guava.common.annotations;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.base;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.base.internal;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.cache;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.collect;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.eventbus;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.io;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.net;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.primitives;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.util.concurrent;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.streamhtmlparser;version=&quot;0.0.10.vaadin1&quot;, com.google.gwt.thirdparty.streamhtmlparser.impl;version=&quot;0.0.10.vaadin1&quot;, com.google.gwt.thirdparty.streamhtmlparser.util;version=&quot;0.0.10.vaadin1&quot;, org.w3c.flute.parser;version=&quot;1.3.0.gg2&quot;, org.w3c.flute.parser.selectors;version=&quot;1.3.0.gg2&quot;, org.w3c.flute.util;version=&quot;1.3.0.gg2&quot;" />
+ <property name="shared.osgi.import"
+ value="com.google.gwt.thirdparty.guava.common.annotations;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.base;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.base.internal;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.cache;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.collect;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.eventbus;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.io;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.net;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.primitives;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.guava.common.util.concurrent;version=&quot;16.0.1.vaadin1&quot;, com.google.gwt.thirdparty.streamhtmlparser;version=&quot;0.0.10.vaadin1&quot;, com.google.gwt.thirdparty.streamhtmlparser.impl;version=&quot;0.0.10.vaadin1&quot;, com.google.gwt.thirdparty.streamhtmlparser.util;version=&quot;0.0.10.vaadin1&quot;, org.w3c.flute.parser;version=&quot;1.3.0.gg2&quot;, org.w3c.flute.parser.selectors;version=&quot;1.3.0.gg2&quot;, org.w3c.flute.util;version=&quot;1.3.0.gg2&quot;" />
<delete dir="${src.filtered}" />
<!-- Update version in Version.java -->
<copy todir="${src.filtered}">
@@ -34,6 +39,7 @@
<antcall target="common.jar">
<param name="import-package" value="${shared.osgi.import}" />
<reference refid="shared.gwt.includes" torefid="extra.jar.includes" />
+ <param name="osgi.extra.package.prefixes" value="elemental" />
</antcall>
</target>
diff --git a/shared/src/com/vaadin/shared/AbstractComponentState.java b/shared/src/com/vaadin/shared/AbstractComponentState.java
index 9e21954f3e..1c32a67c70 100644
--- a/shared/src/com/vaadin/shared/AbstractComponentState.java
+++ b/shared/src/com/vaadin/shared/AbstractComponentState.java
@@ -18,6 +18,7 @@ package com.vaadin.shared;
import java.util.List;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.SharedState;
/**
@@ -31,7 +32,9 @@ public class AbstractComponentState extends SharedState {
public String height = "";
public String width = "";
public boolean readOnly = false;
+ @NoLayout
public boolean immediate = false;
+ @NoLayout
public String description = "";
// Note: for the caption, there is a difference between null and an empty
// string!
diff --git a/shared/src/com/vaadin/shared/annotations/NoLayout.java b/shared/src/com/vaadin/shared/annotations/NoLayout.java
new file mode 100644
index 0000000000..b77729cdcc
--- /dev/null
+++ b/shared/src/com/vaadin/shared/annotations/NoLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used to mark client RPC methods, state fields, or state setter
+ * methods that should not trigger an layout phase after changes have been
+ * processed. Whenever there's at least one change that is not marked with this
+ * annotation, the framework will assume some sizes might have changed an will
+ * therefore start a layout phase after applying the changes.
+ * <p>
+ * This annotation can be used for any RPC method or state property that does
+ * not cause the size of the component or its children to change. Please note
+ * that almost anything related to CSS (e.g. adding or removing a stylename) has
+ * the potential of causing sizes to change with appropriate style definitions
+ * in the application theme.
+ *
+ * @since 7.4
+ *
+ * @author Vaadin Ltd
+ */
+@Documented
+@Target({ ElementType.METHOD, ElementType.FIELD })
+public @interface NoLayout {
+ // Just an empty marker annotation
+}
diff --git a/shared/src/com/vaadin/shared/annotations/NoLoadingIndicator.java b/shared/src/com/vaadin/shared/annotations/NoLoadingIndicator.java
new file mode 100644
index 0000000000..2e519b69e8
--- /dev/null
+++ b/shared/src/com/vaadin/shared/annotations/NoLoadingIndicator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used to mark server RPC methods for which it isn't necessary to
+ * show the loading indicator. The framework will show a loading indicator when
+ * sending requests for RPC methods that are not marked with this annotation.
+ * The loading indicator is hidden once a response is received.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+@Target(ElementType.METHOD)
+@Documented
+public @interface NoLoadingIndicator {
+ // Just an empty marker annotation
+}
diff --git a/shared/src/com/vaadin/shared/communication/SharedState.java b/shared/src/com/vaadin/shared/communication/SharedState.java
index e16fc51fae..b21a675a4a 100644
--- a/shared/src/com/vaadin/shared/communication/SharedState.java
+++ b/shared/src/com/vaadin/shared/communication/SharedState.java
@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Set;
import com.vaadin.shared.Connector;
+import com.vaadin.shared.annotations.NoLayout;
/**
* Interface to be implemented by all shared state classes used to communicate
@@ -64,6 +65,7 @@ public class SharedState implements Serializable {
/**
* A set of event identifiers with registered listeners.
*/
+ @NoLayout
public Set<String> registeredEventListeners = null;
}
diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
new file mode 100644
index 0000000000..4bfdb8b6c5
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.data;
+
+import com.vaadin.shared.annotations.NoLayout;
+import com.vaadin.shared.communication.ClientRpc;
+
+import elemental.json.JsonArray;
+
+/**
+ * RPC interface used for pushing container data to the client.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface DataProviderRpc extends ClientRpc {
+
+ /**
+ * Sends updated row data to a client.
+ * <p>
+ * rowDataJson represents a JSON array of JSON objects in the following
+ * format:
+ *
+ * <pre>
+ * [{
+ * "d": [COL_1_JSON, COL_2_json, ...],
+ * "k": "1"
+ * },
+ * ...
+ * ]
+ * </pre>
+ *
+ * where COL_INDEX is the index of the column (as a string), and COL_n_JSON
+ * is valid JSON of the column's data.
+ *
+ * @param firstRowIndex
+ * the index of the first updated row
+ * @param rowDataJson
+ * the updated row data
+ * @see com.vaadin.shared.ui.grid.GridState#JSONKEY_DATA
+ * @see com.vaadin.ui.components.grid.Renderer#encode(Object)
+ */
+ @NoLayout
+ public void setRowData(int firstRowIndex, JsonArray rowDataJson);
+
+ /**
+ * Informs the client to remove row data.
+ *
+ * @param firstRowIndex
+ * the index of the first removed row
+ * @param count
+ * the number of rows removed from <code>firstRowIndex</code> and
+ * onwards
+ */
+ @NoLayout
+ public void removeRowData(int firstRowIndex, int count);
+
+ /**
+ * Informs the client to insert new row data.
+ *
+ * @param firstRowIndex
+ * the index of the first new row
+ * @param count
+ * the number of rows inserted at <code>firstRowIndex</code>
+ */
+ @NoLayout
+ public void insertRowData(int firstRowIndex, int count);
+
+ /**
+ * Resets all data and defines a new size for the data.
+ * <p>
+ * This should be used in the cases where the data has changed in some
+ * unverifiable way. I.e. "something happened". This will lead to a
+ * re-rendering of the current Grid viewport
+ *
+ * @param size
+ * the size of the new data set
+ */
+ public void resetDataAndSize(int size);
+}
diff --git a/shared/src/com/vaadin/shared/data/DataRequestRpc.java b/shared/src/com/vaadin/shared/data/DataRequestRpc.java
new file mode 100644
index 0000000000..0d9b919a4e
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataRequestRpc.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.data;
+
+import com.vaadin.shared.annotations.NoLoadingIndicator;
+import com.vaadin.shared.annotations.Delayed;
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * RPC interface used for requesting container data to the client.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface DataRequestRpc extends ServerRpc {
+
+ /**
+ * Request rows from the server.
+ *
+ * @param firstRowIndex
+ * the index of the first requested row
+ * @param numberOfRows
+ * the number of requested rows
+ * @param firstCachedRowIndex
+ * the index of the first cached row
+ * @param cacheSize
+ * the number of cached rows
+ */
+ @NoLoadingIndicator
+ public void requestRows(int firstRowIndex, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize);
+
+ /**
+ * Informs the server that an item referenced with a key pinned status has
+ * changed. This is a delayed call that happens along with next rpc call to
+ * server.
+ *
+ * @param key
+ * key mapping to item
+ * @param isPinned
+ * pinned status of referenced item
+ */
+ @Delayed
+ public void setPinned(String key, boolean isPinned);
+}
diff --git a/shared/src/com/vaadin/shared/data/sort/SortDirection.java b/shared/src/com/vaadin/shared/data/sort/SortDirection.java
new file mode 100644
index 0000000000..cd572087d7
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/sort/SortDirection.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.data.sort;
+
+import java.io.Serializable;
+
+/**
+ * Describes sorting direction.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public enum SortDirection implements Serializable {
+
+ /**
+ * Ascending (e.g. A-Z, 1..9) sort order
+ */
+ ASCENDING {
+ @Override
+ public SortDirection getOpposite() {
+ return DESCENDING;
+ }
+ },
+
+ /**
+ * Descending (e.g. Z-A, 9..1) sort order
+ */
+ DESCENDING {
+ @Override
+ public SortDirection getOpposite() {
+ return ASCENDING;
+ }
+ };
+
+ /**
+ * Get the sort direction that is the direct opposite to this one.
+ *
+ * @return a sort direction value
+ */
+ public abstract SortDirection getOpposite();
+}
diff --git a/shared/src/com/vaadin/shared/ui/AbstractEmbeddedState.java b/shared/src/com/vaadin/shared/ui/AbstractEmbeddedState.java
index f5779de43d..0cb9be8702 100644
--- a/shared/src/com/vaadin/shared/ui/AbstractEmbeddedState.java
+++ b/shared/src/com/vaadin/shared/ui/AbstractEmbeddedState.java
@@ -16,10 +16,12 @@
package com.vaadin.shared.ui;
import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.NoLayout;
public class AbstractEmbeddedState extends AbstractComponentState {
public static final String SOURCE_RESOURCE = "source";
+ @NoLayout
public String alternateText;
}
diff --git a/shared/src/com/vaadin/shared/ui/AbstractMediaState.java b/shared/src/com/vaadin/shared/ui/AbstractMediaState.java
index d2ef09810b..3029bedca7 100644
--- a/shared/src/com/vaadin/shared/ui/AbstractMediaState.java
+++ b/shared/src/com/vaadin/shared/ui/AbstractMediaState.java
@@ -19,17 +19,21 @@ import java.util.ArrayList;
import java.util.List;
import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.URLReference;
public class AbstractMediaState extends AbstractComponentState {
public boolean showControls;
+ @NoLayout
public String altText;
public boolean htmlContentAllowed;
+ @NoLayout
public boolean autoplay;
+ @NoLayout
public boolean muted;
public List<URLReference> sources = new ArrayList<URLReference>();
diff --git a/shared/src/com/vaadin/shared/ui/MediaControl.java b/shared/src/com/vaadin/shared/ui/MediaControl.java
index 2311d57b16..ab31d6f95f 100644
--- a/shared/src/com/vaadin/shared/ui/MediaControl.java
+++ b/shared/src/com/vaadin/shared/ui/MediaControl.java
@@ -16,6 +16,7 @@
package com.vaadin.shared.ui;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.ClientRpc;
/**
@@ -27,10 +28,12 @@ public interface MediaControl extends ClientRpc {
/**
* Start playing the media.
*/
+ @NoLayout
public void play();
/**
* Pause playback of the media.
*/
+ @NoLayout
public void pause();
}
diff --git a/shared/src/com/vaadin/shared/ui/TabIndexState.java b/shared/src/com/vaadin/shared/ui/TabIndexState.java
index eec61a0595..1afe982546 100644
--- a/shared/src/com/vaadin/shared/ui/TabIndexState.java
+++ b/shared/src/com/vaadin/shared/ui/TabIndexState.java
@@ -16,6 +16,7 @@
package com.vaadin.shared.ui;
import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.NoLayout;
/**
* Interface implemented by state classes that support tab indexes.
@@ -29,6 +30,7 @@ public class TabIndexState extends AbstractComponentState {
/**
* The <i>tabulator index</i> of the field.
*/
+ @NoLayout
public int tabIndex = 0;
}
diff --git a/shared/src/com/vaadin/shared/ui/button/ButtonState.java b/shared/src/com/vaadin/shared/ui/button/ButtonState.java
index 5b099572df..01ef9a038b 100644
--- a/shared/src/com/vaadin/shared/ui/button/ButtonState.java
+++ b/shared/src/com/vaadin/shared/ui/button/ButtonState.java
@@ -17,6 +17,7 @@
package com.vaadin.shared.ui.button;
import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.ui.TabIndexState;
/**
@@ -31,7 +32,10 @@ public class ButtonState extends TabIndexState {
{
primaryStyleName = "v-button";
}
+ @NoLayout
public boolean disableOnClick = false;
+ @NoLayout
public int clickShortcutKeyCode = 0;
+ @NoLayout
public String iconAltText = "";
}
diff --git a/shared/src/com/vaadin/shared/ui/datefield/PopupDateFieldState.java b/shared/src/com/vaadin/shared/ui/datefield/PopupDateFieldState.java
index 07726f8af0..6f10af4314 100644
--- a/shared/src/com/vaadin/shared/ui/datefield/PopupDateFieldState.java
+++ b/shared/src/com/vaadin/shared/ui/datefield/PopupDateFieldState.java
@@ -15,6 +15,8 @@
*/
package com.vaadin.shared.ui.datefield;
+import com.vaadin.shared.annotations.NoLayout;
+
public class PopupDateFieldState extends TextualDateFieldState {
public static final String DESCRIPTION_FOR_ASSISTIVE_DEVICES = "Arrow down key opens calendar element for choosing the date";
@@ -23,5 +25,6 @@ public class PopupDateFieldState extends TextualDateFieldState {
}
public boolean textFieldEnabled = true;
+ @NoLayout
public String descriptionForAssistiveDevices = DESCRIPTION_FOR_ASSISTIVE_DEVICES;
}
diff --git a/shared/src/com/vaadin/shared/ui/datefield/TextualDateFieldState.java b/shared/src/com/vaadin/shared/ui/datefield/TextualDateFieldState.java
index 09bfb9c1a1..bf38ee04a9 100644
--- a/shared/src/com/vaadin/shared/ui/datefield/TextualDateFieldState.java
+++ b/shared/src/com/vaadin/shared/ui/datefield/TextualDateFieldState.java
@@ -18,6 +18,7 @@ package com.vaadin.shared.ui.datefield;
import java.util.Date;
import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.annotations.NoLayout;
public class TextualDateFieldState extends AbstractFieldState {
{
@@ -28,11 +29,13 @@ public class TextualDateFieldState extends AbstractFieldState {
* Start range that has been cleared, depending on the resolution of the
* date field
*/
+ @NoLayout
public Date rangeStart = null;
/*
* End range that has been cleared, depending on the resolution of the date
* field
*/
+ @NoLayout
public Date rangeEnd = null;
}
diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java
new file mode 100644
index 0000000000..79e6c9a89f
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The column group data shared between the server and the client
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ColumnGroupState implements Serializable {
+
+ /**
+ * The columns that is included in the group
+ */
+ public List<String> columns = new ArrayList<String>();
+
+ /**
+ * The header text of the group
+ */
+ public String header;
+
+ /**
+ * The footer text of the group
+ */
+ public String footer;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/EditorClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/EditorClientRpc.java
new file mode 100644
index 0000000000..82e08999b4
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/EditorClientRpc.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+/**
+ * An RPC interface for the grid editor server-to-client communications.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface EditorClientRpc extends ClientRpc {
+
+ /**
+ * Tells the client to open the editor and bind data to it.
+ *
+ * @param rowIndex
+ * the index of the edited row
+ */
+ void bind(int rowIndex);
+
+ /**
+ * Tells the client to cancel editing and hide the editor.
+ *
+ * @param rowIndex
+ * the index of the edited row
+ */
+ void cancel(int rowIndex);
+
+ /**
+ * Confirms a pending {@link EditorServerRpc#bind(int) bind request} sent by
+ * the client.
+ *
+ * @param bindSucceeded
+ * <code>true</code> iff the bind action was successful
+ */
+ void confirmBind(boolean bindSucceeded);
+
+ /**
+ * Confirms a pending {@link EditorServerRpc#save(int) save request} sent by
+ * the client.
+ *
+ * @param saveSucceeded
+ * <code>true</code> iff the save action was successful
+ */
+ void confirmSave(boolean saveSucceeded);
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/EditorServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/EditorServerRpc.java
new file mode 100644
index 0000000000..34a16ccb38
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/EditorServerRpc.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * An RPC interface for the grid editor client-to-server communications.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface EditorServerRpc extends ServerRpc {
+
+ /**
+ * Asks the server to open the editor and bind data to it. When a bind
+ * request is sent, it must be acknowledged with a
+ * {@link EditorClientRpc#confirmBind() confirm call} before the client can
+ * open the editor.
+ *
+ * @param rowIndex
+ * the index of the edited row
+ */
+ void bind(int rowIndex);
+
+ /**
+ * Asks the server to save unsaved changes in the editor to the data source.
+ * When a save request is sent, it must be acknowledged with a
+ * {@link EditorClientRpc#confirmSave() confirm call}.
+ *
+ * @param rowIndex
+ * the index of the edited row
+ */
+ void save(int rowIndex);
+
+ /**
+ * Tells the server to cancel editing. When sending a cancel request, the
+ * client does not need to wait for confirmation by the server before hiding
+ * the editor.
+ *
+ * @param rowIndex
+ * the index of the edited row
+ */
+ void cancel(int rowIndex);
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
new file mode 100644
index 0000000000..ed849cb361
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+/**
+ * Server-to-client RPC interface for the Grid component.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface GridClientRpc extends ClientRpc {
+
+ /**
+ * Command client Grid to scroll to a specific data row.
+ *
+ * @param row
+ * zero-based row index. If the row index is below zero or above
+ * the row count of the client-side data source, a client-side
+ * exception will be triggered. Since this exception has no
+ * handling by default, an out-of-bounds value will cause a
+ * client-side crash.
+ * @param destination
+ * desired placement of scrolled-to row. See the documentation
+ * for {@link ScrollDestination} for more information.
+ */
+ public void scrollToRow(int row, ScrollDestination destination);
+
+ /**
+ * Command client Grid to scroll to the first row.
+ */
+ public void scrollToStart();
+
+ /**
+ * Command client Grid to scroll to the last row.
+ */
+ public void scrollToEnd();
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
new file mode 100644
index 0000000000..070d146736
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.Connector;
+
+/**
+ * Column state DTO for transferring column properties from the server to the
+ * client
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridColumnState implements Serializable {
+
+ /**
+ * Id used by grid connector to map server side column with client side
+ * column
+ */
+ public String id;
+
+ /**
+ * Column width in pixels. Default column width is
+ * {@value GridConstants#DEFAULT_COLUMN_WIDTH_PX}.
+ */
+ public double width = GridConstants.DEFAULT_COLUMN_WIDTH_PX;
+
+ /**
+ * The connector for the renderer used to render the cells in this column.
+ */
+ public Connector rendererConnector;
+
+ /**
+ * The connector for the field used to edit cells in this column when the
+ * editor interface is active.
+ */
+ public Connector editorConnector;
+
+ /**
+ * Are sorting indicators shown for a column. Default is false.
+ */
+ public boolean sortable = false;
+
+ /** How much of the remaining space this column will reserve. */
+ public int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO;
+
+ /**
+ * The maximum expansion width of this column. -1 for "no maximum". If
+ * maxWidth is less than the calculated width, maxWidth is ignored.
+ */
+ public double maxWidth = GridConstants.DEFAULT_MAX_WIDTH;
+
+ /**
+ * The minimum expansion width of this column. -1 for "no minimum". If
+ * minWidth is less than the calculated width, minWidth will win.
+ */
+ public double minWidth = GridConstants.DEFAULT_MIN_WIDTH;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridConstants.java b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java
new file mode 100644
index 0000000000..b36a162476
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.io.Serializable;
+
+/**
+ * Container class for common constants and default values used by the Grid
+ * component.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public final class GridConstants implements Serializable {
+
+ /**
+ * Default padding in pixels when scrolling programmatically, without an
+ * explicitly defined padding value.
+ */
+ public static final int DEFAULT_PADDING = 0;
+
+ /**
+ * Delay before a long tap action is triggered. Number in milliseconds.
+ */
+ public static final int LONG_TAP_DELAY = 500;
+
+ /**
+ * The threshold in pixels a finger can move while long tapping.
+ */
+ public static final int LONG_TAP_THRESHOLD = 3;
+
+ /* Column constants */
+
+ /**
+ * Default maximum width for columns.
+ */
+ public static final double DEFAULT_MAX_WIDTH = -1;
+
+ /**
+ * Default minimum width for columns.
+ */
+ public static final double DEFAULT_MIN_WIDTH = 10.0d;
+
+ /**
+ * Default expand ratio for columns.
+ */
+ public static final int DEFAULT_EXPAND_RATIO = -1;
+
+ /**
+ * Default width for columns.
+ */
+ public static final double DEFAULT_COLUMN_WIDTH_PX = -1;
+
+ /**
+ * Event ID for item click events
+ */
+ public static final String ITEM_CLICK_EVENT_ID = "itemClick";
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
new file mode 100644
index 0000000000..c90a016383
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.util.List;
+
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * Client-to-server RPC interface for the Grid component
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface GridServerRpc extends ServerRpc {
+
+ void select(List<String> newSelection);
+
+ void selectAll();
+
+ void sort(String[] columnIds, SortDirection[] directions,
+ boolean userOriginated);
+
+ /**
+ * Informs the server that an item has been clicked in Grid.
+ *
+ * @param rowKey
+ * a key identifying the clicked item
+ * @param columnId
+ * column id identifying the clicked property
+ * @param details
+ * mouse event details
+ */
+ void itemClick(String rowKey, String columnId, MouseEventDetails details);
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java
new file mode 100644
index 0000000000..2b18d5b642
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.DelegateToWidget;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridState extends AbstractComponentState {
+
+ /**
+ * A description of which of the three bundled SelectionModels is currently
+ * in use.
+ * <p>
+ * Used as a data transfer object instead of the client/server ones, because
+ * they don't know about each others classes.
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode
+ */
+ public enum SharedSelectionMode {
+ /**
+ * Representation of a single selection mode
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode#SINGLE
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode#SINGLE
+ */
+ SINGLE,
+
+ /**
+ * Representation of a multiselection mode
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode#MULTI
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode#MULTI
+ */
+ MULTI,
+
+ /**
+ * Representation of a no-selection mode
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode#NONE
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode#NONE
+ */
+ NONE;
+ }
+
+ /**
+ * The default value for height-by-rows for both GWT widgets
+ * {@link com.vaadin.ui.components.grid Grid} and
+ * {@link com.vaadin.client.ui.grid.Escalator Escalator}
+ */
+ public static final double DEFAULT_HEIGHT_BY_ROWS = 10.0d;
+
+ /**
+ * The key in which a row's data can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_DATA = "d";
+
+ /**
+ * The key in which a row's own key can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_ROWKEY = "k";
+
+ /**
+ * The key in which a row's generated style can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_ROWSTYLE = "rs";
+
+ /**
+ * The key in which a generated styles for a row's cells can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_CELLSTYLES = "cs";
+
+ /**
+ * Columns in grid.
+ */
+ public List<GridColumnState> columns = new ArrayList<GridColumnState>();
+
+ /**
+ * Column order in grid.
+ */
+ public List<String> columnOrder = new ArrayList<String>();
+
+ public GridStaticSectionState header = new GridStaticSectionState();
+
+ public GridStaticSectionState footer = new GridStaticSectionState();
+
+ /** The number of frozen columns */
+ public int frozenColumnCount = 0;
+
+ /** The height of the Grid in terms of body rows. */
+ @DelegateToWidget
+ public double heightByRows = DEFAULT_HEIGHT_BY_ROWS;
+
+ /** The mode by which Grid defines its height. */
+ @DelegateToWidget
+ public HeightMode heightMode = HeightMode.CSS;
+
+ // instantiated just to avoid NPEs
+ public List<String> selectedKeys = new ArrayList<String>();
+
+ public SharedSelectionMode selectionMode;
+
+ /** Keys of the currently sorted columns */
+ public String[] sortColumns = new String[0];
+
+ /** Directions for each sorted column */
+ public SortDirection[] sortDirs = new SortDirection[0];
+
+ /** The enabled state of the editor interface */
+ public boolean editorEnabled = false;
+
+ /** Whether row data might contain generated row styles */
+ public boolean hasRowStyleGenerator;
+ /** Whether row data might contain generated cell styles */
+ public boolean hasCellStyleGenerator;
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java b/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java
new file mode 100644
index 0000000000..c646717d2c
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+/**
+ * Enumeration, specifying the content type of a Cell in a GridStaticSection.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public enum GridStaticCellType {
+ /**
+ * Text content
+ */
+ TEXT,
+
+ /**
+ * HTML content
+ */
+ HTML,
+
+ /**
+ * Widget content
+ */
+ WIDGET;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java b/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java
new file mode 100644
index 0000000000..a3c485af08
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.shared.Connector;
+
+/**
+ * Shared state for Grid headers and footers.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class GridStaticSectionState implements Serializable {
+
+ public static class CellState implements Serializable {
+ public String text = "";
+
+ public String html = "";
+
+ public Connector connector = null;
+
+ public GridStaticCellType type = GridStaticCellType.TEXT;
+
+ public String columnId;
+
+ public String styleName = null;
+ }
+
+ public static class RowState implements Serializable {
+ public List<CellState> cells = new ArrayList<CellState>();
+
+ public boolean defaultRow = false;
+
+ /**
+ * Map from column id set to cell state for merged state.
+ */
+ public Map<Set<String>, CellState> cellGroups = new HashMap<Set<String>, CellState>();
+
+ /**
+ * The style name for the row. Null if none.
+ */
+ public String styleName = null;
+ }
+
+ public List<RowState> rows = new ArrayList<RowState>();
+
+ public boolean visible = true;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/HeightMode.java b/shared/src/com/vaadin/shared/ui/grid/HeightMode.java
new file mode 100644
index 0000000000..4cd19a01b1
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/HeightMode.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+/**
+ * The modes for height calculation that are supported by Grid (
+ * {@link com.vaadin.client.ui.grid.Grid client} and
+ * {@link com.vaadin.ui.components.grid.Grid server}) /
+ * {@link com.vaadin.client.ui.grid.Escalator Escalator}.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ * @see com.vaadin.client.ui.grid.Grid#setHeightMode(HeightMode)
+ * @see com.vaadin.ui.components.grid.Grid#setHeightMode(HeightMode)
+ * @see com.vaadin.client.ui.grid.Escalator#setHeightMode(HeightMode)
+ */
+public enum HeightMode {
+ /**
+ * The height of the Component or Widget is defined by a CSS-like value.
+ * (e.g. "100px", "50em" or "25%")
+ */
+ CSS,
+
+ /**
+ * The height of the Component or Widget in question is defined by a number
+ * of rows.
+ */
+ ROW;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java
new file mode 100644
index 0000000000..21e70d3dbf
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/Range.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import java.io.Serializable;
+
+/**
+ * An immutable representation of a range, marked by start and end points.
+ * <p>
+ * The range is treated as inclusive at the start, and exclusive at the end.
+ * I.e. the range [0..1[ has the length 1, and represents one integer: 0.
+ * <p>
+ * The range is considered {@link #isEmpty() empty} if the start is the same as
+ * the end.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public final class Range implements Serializable {
+ private final int start;
+ private final int end;
+
+ /**
+ * Creates a range object representing a single integer.
+ *
+ * @param integer
+ * the number to represent as a range
+ * @return the range represented by <code>integer</code>
+ */
+ public static Range withOnly(final int integer) {
+ return new Range(integer, integer + 1);
+ }
+
+ /**
+ * Creates a range between two integers.
+ * <p>
+ * The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
+ * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
+ * but not 5.
+ *
+ * @param start
+ * the start of the the range, inclusive
+ * @param end
+ * the end of the range, exclusive
+ * @return a range representing <code>[start..end[</code>
+ * @throws IllegalArgumentException
+ * if <code>start &gt; end</code>
+ */
+ public static Range between(final int start, final int end)
+ throws IllegalArgumentException {
+ return new Range(start, end);
+ }
+
+ /**
+ * Creates a range from a start point, with a given length.
+ *
+ * @param start
+ * the first integer to include in the range
+ * @param length
+ * the length of the resulting range
+ * @return a range starting from <code>start</code>, with
+ * <code>length</code> number of integers following
+ * @throws IllegalArgumentException
+ * if length &lt; 0
+ */
+ public static Range withLength(final int start, final int length)
+ throws IllegalArgumentException {
+ if (length < 0) {
+ /*
+ * The constructor of Range will throw an exception if start >
+ * start+length (i.e. if length is negative). We're throwing the
+ * same exception type, just with a more descriptive message.
+ */
+ throw new IllegalArgumentException("length must not be negative");
+ }
+ return new Range(start, start + length);
+ }
+
+ /**
+ * Creates a new range between two numbers: <code>[start..end[</code>.
+ *
+ * @param start
+ * the start integer, inclusive
+ * @param end
+ * the end integer, exclusive
+ * @throws IllegalArgumentException
+ * if <code>start &gt; end</code>
+ */
+ private Range(final int start, final int end)
+ throws IllegalArgumentException {
+ if (start > end) {
+ throw new IllegalArgumentException(
+ "start must not be greater than end");
+ }
+
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Returns the <em>inclusive</em> start point of this range.
+ *
+ * @return the start point of this range
+ */
+ public int getStart() {
+ return start;
+ }
+
+ /**
+ * Returns the <em>exclusive</em> end point of this range.
+ *
+ * @return the end point of this range
+ */
+ public int getEnd() {
+ return end;
+ }
+
+ /**
+ * The number of integers contained in the range.
+ *
+ * @return the number of integers contained in the range
+ */
+ public int length() {
+ return getEnd() - getStart();
+ }
+
+ /**
+ * Checks whether the range has no elements between the start and end.
+ *
+ * @return <code>true</code> iff the range contains no elements.
+ */
+ public boolean isEmpty() {
+ return getStart() >= getEnd();
+ }
+
+ /**
+ * Checks whether this range and another range are at least partially
+ * covering the same values.
+ *
+ * @param other
+ * the other range to check against
+ * @return <code>true</code> if this and <code>other</code> intersect
+ */
+ public boolean intersects(final Range other) {
+ return getStart() < other.getEnd() && other.getStart() < getEnd();
+ }
+
+ /**
+ * Checks whether an integer is found within this range.
+ *
+ * @param integer
+ * an integer to test for presence in this range
+ * @return <code>true</code> iff <code>integer</code> is in this range
+ */
+ public boolean contains(final int integer) {
+ return getStart() <= integer && integer < getEnd();
+ }
+
+ /**
+ * Checks whether this range is a subset of another range.
+ *
+ * @return <code>true</code> iff <code>other</code> completely wraps this
+ * range
+ */
+ public boolean isSubsetOf(final Range other) {
+ if (isEmpty() && other.isEmpty()) {
+ return true;
+ }
+
+ return other.getStart() <= getStart() && getEnd() <= other.getEnd();
+ }
+
+ /**
+ * Overlay this range with another one, and partition the ranges according
+ * to how they position relative to each other.
+ * <p>
+ * The three partitions are returned as a three-element Range array:
+ * <ul>
+ * <li>Elements in this range that occur before elements in
+ * <code>other</code>.
+ * <li>Elements that are shared between the two ranges.
+ * <li>Elements in this range that occur after elements in
+ * <code>other</code>.
+ * </ul>
+ *
+ * @param other
+ * the other range to act as delimiters.
+ * @return a three-element Range array of partitions depicting the elements
+ * before (index 0), shared/inside (index 1) and after (index 2).
+ */
+ public Range[] partitionWith(final Range other) {
+ final Range[] splitBefore = splitAt(other.getStart());
+ final Range rangeBefore = splitBefore[0];
+ final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd());
+ final Range rangeInside = splitAfter[0];
+ final Range rangeAfter = splitAfter[1];
+ return new Range[] { rangeBefore, rangeInside, rangeAfter };
+ }
+
+ /**
+ * Get a range that is based on this one, but offset by a number.
+ *
+ * @param offset
+ * the number to offset by
+ * @return a copy of this range, offset by <code>offset</code>
+ */
+ public Range offsetBy(final int offset) {
+ if (offset == 0) {
+ return this;
+ } else {
+ return new Range(start + offset, end + offset);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
+ + "[" + (isEmpty() ? " (empty)" : "");
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + end;
+ result = prime * result + start;
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Range other = (Range) obj;
+ if (end != other.end) {
+ return false;
+ }
+ if (start != other.start) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether this range starts before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range starts before the
+ * <code>other</code>
+ */
+ public boolean startsBefore(final Range other) {
+ return getStart() < other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range ends before the
+ * <code>other</code>
+ */
+ public boolean endsBefore(final Range other) {
+ return getEnd() <= other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range ends after the
+ * <code>other</code>
+ */
+ public boolean endsAfter(final Range other) {
+ return getEnd() > other.getEnd();
+ }
+
+ /**
+ * Checks whether this range starts after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range starts after the
+ * <code>other</code>
+ */
+ public boolean startsAfter(final Range other) {
+ return getStart() >= other.getEnd();
+ }
+
+ /**
+ * Split the range into two at a certain integer.
+ * <p>
+ * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
+ *
+ * @param integer
+ * the integer at which to split the range into two
+ * @return an array of two ranges, with <code>[start..integer[</code> in the
+ * first element, and <code>[integer..end[</code> in the second
+ * element.
+ * <p>
+ * If {@code integer} is less than {@code start}, [empty,
+ * {@code this} ] is returned. if <code>integer</code> is equal to
+ * or greater than {@code end}, [{@code this}, empty] is returned
+ * instead.
+ */
+ public Range[] splitAt(final int integer) {
+ if (integer < start) {
+ return new Range[] { Range.withLength(start, 0), this };
+ } else if (integer >= end) {
+ return new Range[] { this, Range.withLength(end, 0) };
+ } else {
+ return new Range[] { new Range(start, integer),
+ new Range(integer, end) };
+ }
+ }
+
+ /**
+ * Split the range into two after a certain number of integers into the
+ * range.
+ * <p>
+ * Calling this method is equivalent to calling
+ * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
+ * <p>
+ * <em>Example:</em>
+ * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
+ *
+ * @param length
+ * the length at which to split this range into two
+ * @return an array of two ranges, having the <code>length</code>-first
+ * elements of this range, and the second range having the rest. If
+ * <code>length</code> &leq; 0, the first element will be empty, and
+ * the second element will be this range. If <code>length</code>
+ * &geq; {@link #length()}, the first element will be this range,
+ * and the second element will be empty.
+ */
+ public Range[] splitAtFromStart(final int length) {
+ return splitAt(getStart() + length);
+ }
+
+ /**
+ * Combines two ranges to create a range containing all values in both
+ * ranges, provided there are no gaps between the ranges.
+ *
+ * @param other
+ * the range to combine with this range
+ *
+ * @return the combined range
+ *
+ * @throws IllegalArgumentException
+ * if the two ranges aren't connected
+ */
+ public Range combineWith(Range other) throws IllegalArgumentException {
+ if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
+ throw new IllegalArgumentException("There is a gap between " + this
+ + " and " + other);
+ }
+
+ return Range.between(Math.min(getStart(), other.getStart()),
+ Math.max(getEnd(), other.getEnd()));
+ }
+
+ /**
+ * Creates a range that is expanded the given amounts in both ends.
+ *
+ * @param startDelta
+ * the amount to expand by in the beginning of the range
+ * @param endDelta
+ * the amount to expand by in the end of the range
+ *
+ * @return an expanded range
+ *
+ * @throws IllegalArgumentException
+ * if the new range would have <code>start &gt; end</code>
+ */
+ public Range expand(int startDelta, int endDelta)
+ throws IllegalArgumentException {
+ return Range.between(getStart() - startDelta, getEnd() + endDelta);
+ }
+
+ /**
+ * Limits this range to be within the bounds of the provided range.
+ * <p>
+ * This is basically an optimized way of calculating
+ * <code>{@link #partitionWith(Range)}[1]</code> without the overhead of
+ * defining the parts that do not overlap.
+ * <p>
+ * If the two ranges do not intersect, an empty range is returned. There are
+ * no guarantees about the position of that range.
+ *
+ * @param bounds
+ * the bounds that the returned range should be limited to
+ * @return a bounded range
+ */
+ public Range restrictTo(Range bounds) {
+ boolean startWithin = bounds.contains(getStart());
+ boolean endWithin = bounds.contains(getEnd());
+ boolean boundsWithin = getStart() < bounds.getStart()
+ && getEnd() >= bounds.getEnd();
+
+ if (startWithin) {
+ if (endWithin) {
+ return this;
+ } else {
+ return Range.between(getStart(), bounds.getEnd());
+ }
+ } else {
+ if (endWithin) {
+ return Range.between(bounds.getStart(), getEnd());
+ } else if (boundsWithin) {
+ return bounds;
+ } else {
+ return Range.withLength(getStart(), 0);
+ }
+ }
+ }
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java
new file mode 100644
index 0000000000..64cf070e46
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+/**
+ * Enumeration, specifying the destinations that are supported when scrolling
+ * rows or columns into view.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public enum ScrollDestination {
+
+ /**
+ * Scroll as little as possible to show the target element. If the element
+ * fits into view, this works as START or END depending on the current
+ * scroll position. If the element does not fit into view, this works as
+ * START.
+ */
+ ANY,
+
+ /**
+ * Scrolls so that the element is shown at the start of the viewport. The
+ * viewport will, however, not scroll beyond its contents.
+ */
+ START,
+
+ /**
+ * Scrolls so that the element is shown in the middle of the viewport. The
+ * viewport will, however, not scroll beyond its contents, given more
+ * elements than what the viewport is able to show at once. Under no
+ * circumstances will the viewport scroll before its first element.
+ */
+ MIDDLE,
+
+ /**
+ * Scrolls so that the element is shown at the end of the viewport. The
+ * viewport will, however, not scroll before its first element.
+ */
+ END
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/renderers/RendererClickRpc.java b/shared/src/com/vaadin/shared/ui/grid/renderers/RendererClickRpc.java
new file mode 100644
index 0000000000..658caef050
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/renderers/RendererClickRpc.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid.renderers;
+
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.communication.ServerRpc;
+
+public interface RendererClickRpc extends ServerRpc {
+ /**
+ * Called when a click event has occurred and there are server side
+ * listeners for the event.
+ *
+ * @param mouseDetails
+ * Details about the mouse when the event took place
+ */
+ public void click(String rowKey, String columnId,
+ MouseEventDetails mouseDetails);
+}
diff --git a/shared/src/com/vaadin/shared/ui/panel/PanelState.java b/shared/src/com/vaadin/shared/ui/panel/PanelState.java
index 6c0fcb683c..8f48fad921 100644
--- a/shared/src/com/vaadin/shared/ui/panel/PanelState.java
+++ b/shared/src/com/vaadin/shared/ui/panel/PanelState.java
@@ -16,11 +16,14 @@
package com.vaadin.shared.ui.panel;
import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.NoLayout;
public class PanelState extends AbstractComponentState {
{
primaryStyleName = "v-panel";
}
+ @NoLayout
public int tabIndex;
+ @NoLayout
public int scrollLeft, scrollTop;
}
diff --git a/shared/src/com/vaadin/shared/ui/popupview/PopupViewState.java b/shared/src/com/vaadin/shared/ui/popupview/PopupViewState.java
index da49e47ae8..86fda428a1 100644
--- a/shared/src/com/vaadin/shared/ui/popupview/PopupViewState.java
+++ b/shared/src/com/vaadin/shared/ui/popupview/PopupViewState.java
@@ -16,10 +16,12 @@
package com.vaadin.shared.ui.popupview;
import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.annotations.NoLayout;
public class PopupViewState extends AbstractComponentState {
public String html;
+ @NoLayout
public boolean hideOnMouseOut;
}
diff --git a/shared/src/com/vaadin/shared/ui/progressindicator/ProgressBarState.java b/shared/src/com/vaadin/shared/ui/progressindicator/ProgressBarState.java
index 79ef766951..6f557489dd 100644
--- a/shared/src/com/vaadin/shared/ui/progressindicator/ProgressBarState.java
+++ b/shared/src/com/vaadin/shared/ui/progressindicator/ProgressBarState.java
@@ -17,6 +17,7 @@
package com.vaadin.shared.ui.progressindicator;
import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.SharedState;
/**
@@ -32,6 +33,7 @@ public class ProgressBarState extends AbstractFieldState {
primaryStyleName = PRIMARY_STYLE_NAME;
}
public boolean indeterminate = false;
+ @NoLayout
public Float state = 0.0f;
}
diff --git a/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorServerRpc.java b/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorServerRpc.java
index dd437094c7..f541395cef 100644
--- a/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorServerRpc.java
+++ b/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorServerRpc.java
@@ -15,8 +15,10 @@
*/
package com.vaadin.shared.ui.progressindicator;
+import com.vaadin.shared.annotations.NoLoadingIndicator;
import com.vaadin.shared.communication.ServerRpc;
public interface ProgressIndicatorServerRpc extends ServerRpc {
+ @NoLoadingIndicator
public void poll();
}
diff --git a/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorState.java b/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorState.java
index 15d0a947d7..9b3cf94d4a 100644
--- a/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorState.java
+++ b/shared/src/com/vaadin/shared/ui/progressindicator/ProgressIndicatorState.java
@@ -15,6 +15,8 @@
*/
package com.vaadin.shared.ui.progressindicator;
+import com.vaadin.shared.annotations.NoLayout;
+
@Deprecated
public class ProgressIndicatorState extends ProgressBarState {
public static final String PRIMARY_STYLE_NAME = "v-progressindicator";
@@ -23,5 +25,6 @@ public class ProgressIndicatorState extends ProgressBarState {
primaryStyleName = PRIMARY_STYLE_NAME;
}
+ @NoLayout
public int pollingInterval = 1000;
}
diff --git a/shared/src/com/vaadin/shared/ui/slider/SliderState.java b/shared/src/com/vaadin/shared/ui/slider/SliderState.java
index 0e48a0c4e2..a96d35bc13 100644
--- a/shared/src/com/vaadin/shared/ui/slider/SliderState.java
+++ b/shared/src/com/vaadin/shared/ui/slider/SliderState.java
@@ -16,21 +16,26 @@
package com.vaadin.shared.ui.slider;
import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.annotations.NoLayout;
public class SliderState extends AbstractFieldState {
{
primaryStyleName = "v-slider";
}
+ @NoLayout
public double value;
+ @NoLayout
public double maxValue = 100;
+ @NoLayout
public double minValue = 0;
/**
* The number of fractional digits that are considered significant. Must be
* non-negative.
*/
+ @NoLayout
public int resolution = 0;
public SliderOrientation orientation = SliderOrientation.HORIZONTAL;
diff --git a/shared/src/com/vaadin/shared/ui/tabsheet/TabsheetState.java b/shared/src/com/vaadin/shared/ui/tabsheet/TabsheetState.java
index f17f214626..6059379dc5 100644
--- a/shared/src/com/vaadin/shared/ui/tabsheet/TabsheetState.java
+++ b/shared/src/com/vaadin/shared/ui/tabsheet/TabsheetState.java
@@ -20,6 +20,7 @@ import java.util.List;
import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.annotations.DelegateToWidget;
+import com.vaadin.shared.annotations.NoLayout;
public class TabsheetState extends AbstractComponentState {
public static final String PRIMARY_STYLE_NAME = "v-tabsheet";
@@ -32,6 +33,7 @@ public class TabsheetState extends AbstractComponentState {
* Index of the component when switching focus - not related to Tabsheet
* tabs.
*/
+ @NoLayout
public int tabIndex;
public List<TabState> tabs = new ArrayList<TabState>();
diff --git a/shared/src/com/vaadin/shared/ui/textarea/TextAreaState.java b/shared/src/com/vaadin/shared/ui/textarea/TextAreaState.java
index 380ee4c7fb..c1f9536278 100644
--- a/shared/src/com/vaadin/shared/ui/textarea/TextAreaState.java
+++ b/shared/src/com/vaadin/shared/ui/textarea/TextAreaState.java
@@ -16,6 +16,7 @@
package com.vaadin.shared.ui.textarea;
import com.vaadin.shared.annotations.DelegateToWidget;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.ui.textfield.AbstractTextFieldState;
public class TextAreaState extends AbstractTextFieldState {
@@ -33,6 +34,7 @@ public class TextAreaState extends AbstractTextFieldState {
* Tells if word-wrapping should be used in the text area.
*/
@DelegateToWidget
+ @NoLayout
public boolean wordwrap = true;
}
diff --git a/shared/src/com/vaadin/shared/ui/textfield/AbstractTextFieldState.java b/shared/src/com/vaadin/shared/ui/textfield/AbstractTextFieldState.java
index 084d02cd7b..9d4272c22f 100644
--- a/shared/src/com/vaadin/shared/ui/textfield/AbstractTextFieldState.java
+++ b/shared/src/com/vaadin/shared/ui/textfield/AbstractTextFieldState.java
@@ -16,6 +16,7 @@
package com.vaadin.shared.ui.textfield;
import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.shared.annotations.NoLayout;
public class AbstractTextFieldState extends AbstractFieldState {
{
@@ -25,6 +26,7 @@ public class AbstractTextFieldState extends AbstractFieldState {
/**
* Maximum character count in text field.
*/
+ @NoLayout
public int maxLength = -1;
/**
@@ -35,10 +37,12 @@ public class AbstractTextFieldState extends AbstractFieldState {
/**
* The prompt to display in an empty field. Null when disabled.
*/
+ @NoLayout
public String inputPrompt = null;
/**
* The text in the field
*/
+ @NoLayout
public String text = null;
}
diff --git a/shared/src/com/vaadin/shared/ui/ui/ScrollClientRpc.java b/shared/src/com/vaadin/shared/ui/ui/ScrollClientRpc.java
index e32a27830d..fb052a25e9 100644
--- a/shared/src/com/vaadin/shared/ui/ui/ScrollClientRpc.java
+++ b/shared/src/com/vaadin/shared/ui/ui/ScrollClientRpc.java
@@ -16,11 +16,14 @@
package com.vaadin.shared.ui.ui;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.ClientRpc;
public interface ScrollClientRpc extends ClientRpc {
+ @NoLayout
public void setScrollTop(int scrollTop);
+ @NoLayout
public void setScrollLeft(int scrollLeft);
}
diff --git a/shared/src/com/vaadin/shared/ui/ui/UIServerRpc.java b/shared/src/com/vaadin/shared/ui/ui/UIServerRpc.java
index 8227415e58..887ea760b3 100644
--- a/shared/src/com/vaadin/shared/ui/ui/UIServerRpc.java
+++ b/shared/src/com/vaadin/shared/ui/ui/UIServerRpc.java
@@ -15,6 +15,7 @@
*/
package com.vaadin.shared.ui.ui;
+import com.vaadin.shared.annotations.NoLoadingIndicator;
import com.vaadin.shared.annotations.Delayed;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.ui.ClickRpc;
@@ -27,6 +28,7 @@ public interface UIServerRpc extends ClickRpc, ServerRpc {
@Delayed(lastOnly = true)
public void scroll(int scrollTop, int scrollLeft);
+ @NoLoadingIndicator
@Delayed(lastOnly = true)
/*
* @Delayed just to get lastOnly semantics, sendPendingVariableChanges()
diff --git a/shared/src/com/vaadin/shared/ui/window/WindowState.java b/shared/src/com/vaadin/shared/ui/window/WindowState.java
index fa73bea391..7dafba57ff 100644
--- a/shared/src/com/vaadin/shared/ui/window/WindowState.java
+++ b/shared/src/com/vaadin/shared/ui/window/WindowState.java
@@ -16,6 +16,7 @@
package com.vaadin.shared.ui.window;
import com.vaadin.shared.Connector;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.ui.panel.PanelState;
public class WindowState extends PanelState {
@@ -23,20 +24,34 @@ public class WindowState extends PanelState {
primaryStyleName = "v-window";
}
+ @NoLayout
public boolean modal = false;
+ @NoLayout
public boolean resizable = true;
+ @NoLayout
public boolean resizeLazy = false;
+ @NoLayout
public boolean draggable = true;
+ @NoLayout
public boolean centered = false;
+ @NoLayout
public int positionX = -1;
+ @NoLayout
public int positionY = -1;
public WindowMode windowMode = WindowMode.NORMAL;
+ @NoLayout
public String assistivePrefix = "";
+ @NoLayout
public String assistivePostfix = "";
+ @NoLayout
public Connector[] contentDescription = new Connector[0];
+ @NoLayout
public WindowRole role = WindowRole.DIALOG;
+ @NoLayout
public boolean assistiveTabStop = false;
+ @NoLayout
public String assistiveTabStopTopText = "Top of dialog";
+ @NoLayout
public String assistiveTabStopBottomText = "Bottom of Dialog";
}
diff --git a/shared/src/com/vaadin/shared/util/SharedUtil.java b/shared/src/com/vaadin/shared/util/SharedUtil.java
index cc98d11abd..206041235a 100644
--- a/shared/src/com/vaadin/shared/util/SharedUtil.java
+++ b/shared/src/com/vaadin/shared/util/SharedUtil.java
@@ -61,6 +61,142 @@ public class SharedUtil implements Serializable {
public static final String SIZE_PATTERN = "^(-?\\d*(?:\\.\\d+)?)(%|px|em|rem|ex|in|cm|mm|pt|pc)?$";
/**
+ * Splits a camelCaseString into an array of words with the casing
+ * preserved.
+ *
+ * @since 7.4
+ * @param camelCaseString
+ * The input string in camelCase format
+ * @return An array with one entry per word in the input string
+ */
+ public static String[] splitCamelCase(String camelCaseString) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < camelCaseString.length(); i++) {
+ char c = camelCaseString.charAt(i);
+ if (Character.isUpperCase(c) && isWordComplete(camelCaseString, i)) {
+ sb.append(' ');
+ }
+ sb.append(c);
+ }
+ return sb.toString().split(" ");
+ }
+
+ private static boolean isWordComplete(String camelCaseString, int i) {
+ if (i == 0) {
+ // Word can't end at the beginning
+ return false;
+ } else if (!Character.isUpperCase(camelCaseString.charAt(i - 1))) {
+ // Word ends if previous char wasn't upper case
+ return true;
+ } else if (i + 1 < camelCaseString.length()
+ && !Character.isUpperCase(camelCaseString.charAt(i + 1))) {
+ // Word ends if next char isn't upper case
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Converts a camelCaseString to a human friendly format (Camel case
+ * string).
+ * <p>
+ * In general splits words when the casing changes but also handles special
+ * cases such as consecutive upper case characters. Examples:
+ * <p>
+ * {@literal MyBeanContainer} becomes {@literal My Bean Container}
+ * {@literal AwesomeURLFactory} becomes {@literal Awesome URL Factory}
+ * {@literal SomeUriAction} becomes {@literal Some Uri Action}
+ *
+ * @since 7.4
+ * @param camelCaseString
+ * The input string in camelCase format
+ * @return A human friendly version of the input
+ */
+ public static String camelCaseToHumanFriendly(String camelCaseString) {
+ String[] parts = splitCamelCase(camelCaseString);
+ for (int i = 0; i < parts.length; i++) {
+ parts[i] = capitalize(parts[i]);
+ }
+ return join(parts, " ");
+ }
+
+ private static boolean isAllUpperCase(String string) {
+ for (int i = 0; i < string.length(); i++) {
+ char c = string.charAt(i);
+ if (!Character.isUpperCase(c) && !Character.isDigit(c)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Joins the words in the input array together into a single string by
+ * inserting the separator string between each word.
+ *
+ * @since 7.4
+ * @param parts
+ * The array of words
+ * @param separator
+ * The separator string to use between words
+ * @return The constructed string of words and separators
+ */
+ public static String join(String[] parts, String separator) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < parts.length; i++) {
+ sb.append(parts[i]);
+ sb.append(separator);
+ }
+ return sb.substring(0, sb.length() - 1);
+ }
+
+ /**
+ * Capitalizes the first character in the given string
+ *
+ * @since 7.4
+ * @param string
+ * The string to capitalize
+ * @return The capitalized string
+ */
+ public static String capitalize(String string) {
+ if (string == null) {
+ return null;
+ }
+
+ if (string.length() <= 1) {
+ return string.toUpperCase();
+ }
+
+ return string.substring(0, 1).toUpperCase() + string.substring(1);
+ }
+
+ /**
+ * Converts a property id to a human friendly format. Handles nested
+ * properties by only considering the last part, e.g. "address.streetName"
+ * is equal to "streetName" for this method.
+ *
+ * @since 7.4
+ * @param propertyId
+ * The propertyId to format
+ * @return A human friendly version of the property id
+ */
+ public static String propertyIdToHumanFriendly(Object propertyId) {
+ String string = propertyId.toString();
+ if (string.isEmpty()) {
+ return "";
+ }
+
+ // For nested properties, only use the last part
+ int dotLocation = string.lastIndexOf('.');
+ if (dotLocation > 0 && dotLocation < string.length() - 1) {
+ string = string.substring(dotLocation + 1);
+ }
+
+ return camelCaseToHumanFriendly(string);
+ }
+
+ /**
* Adds the get parameters to the uri and returns the new uri that contains
* the parameters.
*
@@ -86,18 +222,18 @@ public class SharedUtil implements Serializable {
// 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;
}
diff --git a/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java
new file mode 100644
index 0000000000..e3cae858ee
--- /dev/null
+++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * 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.shared.ui.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+@SuppressWarnings("static-method")
+public class RangeTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void startAfterEndTest() {
+ Range.between(10, 9);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void negativeLengthTest() {
+ Range.withLength(10, -1);
+ }
+
+ @Test
+ public void constructorEquivalenceTest() {
+ assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11));
+ assertEquals("[10,20[ == 10, length 10", Range.between(10, 20),
+ Range.withLength(10, 10));
+ assertEquals("10 == 10, length 1", Range.withOnly(10),
+ Range.withLength(10, 1));
+ }
+
+ @Test
+ public void boundsTest() {
+ {
+ final Range range = Range.between(0, 10);
+ assertEquals("between(0, 10) start", 0, range.getStart());
+ assertEquals("between(0, 10) end", 10, range.getEnd());
+ }
+
+ {
+ final Range single = Range.withOnly(10);
+ assertEquals("withOnly(10) start", 10, single.getStart());
+ assertEquals("withOnly(10) end", 11, single.getEnd());
+ }
+
+ {
+ final Range length = Range.withLength(10, 5);
+ assertEquals("withLength(10, 5) start", 10, length.getStart());
+ assertEquals("withLength(10, 5) end", 15, length.getEnd());
+ }
+ }
+
+ @Test
+ @SuppressWarnings("boxing")
+ public void equalsTest() {
+ final Range range1 = Range.between(0, 10);
+ final Range range2 = Range.withLength(0, 11);
+
+ assertTrue("null", !range1.equals(null));
+ assertTrue("reflexive", range1.equals(range1));
+ assertEquals("symmetric", range1.equals(range2), range2.equals(range1));
+ }
+
+ @Test
+ public void containsTest() {
+ final int start = 0;
+ final int end = 10;
+ final Range range = Range.between(start, end);
+
+ assertTrue("start should be contained", range.contains(start));
+ assertTrue("start-1 should not be contained",
+ !range.contains(start - 1));
+ assertTrue("end should not be contained", !range.contains(end));
+ assertTrue("end-1 should be contained", range.contains(end - 1));
+
+ assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5));
+ assertTrue("empty range does not contain 5", !Range.between(5, 5)
+ .contains(5));
+ }
+
+ @Test
+ public void emptyTest() {
+ assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty());
+ assertTrue("Range of length 0 should be empty", Range.withLength(0, 0)
+ .isEmpty());
+
+ assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty());
+ assertTrue("Range of length 1 should not be empty",
+ !Range.withLength(0, 1).isEmpty());
+ }
+
+ @Test
+ public void splitTest() {
+ final Range startRange = Range.between(0, 10);
+ final Range[] splitRanges = startRange.splitAt(5);
+ assertEquals("[0..10[ split at 5, lower", Range.between(0, 5),
+ splitRanges[0]);
+ assertEquals("[0..10[ split at 5, upper", Range.between(5, 10),
+ splitRanges[1]);
+ }
+
+ @Test
+ public void split_valueBefore() {
+ Range range = Range.between(10, 20);
+ Range[] splitRanges = range.splitAt(5);
+
+ assertEquals(Range.between(10, 10), splitRanges[0]);
+ assertEquals(range, splitRanges[1]);
+ }
+
+ @Test
+ public void split_valueAfter() {
+ Range range = Range.between(10, 20);
+ Range[] splitRanges = range.splitAt(25);
+
+ assertEquals(range, splitRanges[0]);
+ assertEquals(Range.between(20, 20), splitRanges[1]);
+ }
+
+ @Test
+ public void emptySplitTest() {
+ final Range range = Range.between(5, 10);
+ final Range[] split1 = range.splitAt(0);
+ assertTrue("split1, [0]", split1[0].isEmpty());
+ assertEquals("split1, [1]", range, split1[1]);
+
+ final Range[] split2 = range.splitAt(15);
+ assertEquals("split2, [0]", range, split2[0]);
+ assertTrue("split2, [1]", split2[1].isEmpty());
+ }
+
+ @Test
+ public void lengthTest() {
+ assertEquals("withLength length", 5, Range.withLength(10, 5).length());
+ assertEquals("between length", 5, Range.between(10, 15).length());
+ assertEquals("withOnly 10 length", 1, Range.withOnly(10).length());
+ }
+
+ @Test
+ public void intersectsTest() {
+ assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10)
+ .intersects(Range.between(5, 15)));
+ assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10)
+ .intersects(Range.between(10, 20)));
+ }
+
+ @Test
+ public void intersects_emptyInside() {
+ assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5)
+ .intersects(Range.between(0, 10)));
+ assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10)
+ .intersects(Range.between(5, 5)));
+ }
+
+ @Test
+ public void intersects_emptyOutside() {
+ assertTrue("[15..15[ does not intersect with [0..10[",
+ !Range.between(15, 15).intersects(Range.between(0, 10)));
+ assertTrue("[0..10[ does not intersect with [15..15[",
+ !Range.between(0, 10).intersects(Range.between(15, 15)));
+ }
+
+ @Test
+ public void subsetTest() {
+ assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10)
+ .isSubsetOf(Range.between(0, 20)));
+
+ final Range range = Range.between(0, 10);
+ assertTrue("range is subset of self", range.isSubsetOf(range));
+
+ assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10)
+ .isSubsetOf(Range.between(5, 15)));
+ }
+
+ @Test
+ public void offsetTest() {
+ assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5));
+ }
+
+ @Test
+ public void rangeStartsBeforeTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(1, 5);
+ assertTrue("former should starts before latter",
+ former.startsBefore(latter));
+ assertTrue("latter shouldn't start before latter",
+ !latter.startsBefore(former));
+
+ assertTrue("no overlap allowed",
+ !Range.between(0, 5).startsBefore(Range.between(0, 10)));
+ }
+
+ @Test
+ public void rangeStartsAfterTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(5, 10);
+ assertTrue("latter should start after former",
+ latter.startsAfter(former));
+ assertTrue("former shouldn't start after latter",
+ !former.startsAfter(latter));
+
+ assertTrue("no overlap allowed",
+ !Range.between(5, 10).startsAfter(Range.between(0, 6)));
+ }
+
+ @Test
+ public void rangeEndsBeforeTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(5, 10);
+ assertTrue("latter should end before former", former.endsBefore(latter));
+ assertTrue("former shouldn't end before latter",
+ !latter.endsBefore(former));
+
+ assertTrue("no overlap allowed",
+ !Range.between(5, 10).endsBefore(Range.between(9, 15)));
+ }
+
+ @Test
+ public void rangeEndsAfterTest() {
+ final Range former = Range.between(1, 5);
+ final Range latter = Range.between(1, 6);
+ assertTrue("latter should end after former", latter.endsAfter(former));
+ assertTrue("former shouldn't end after latter",
+ !former.endsAfter(latter));
+
+ assertTrue("no overlap allowed",
+ !Range.between(0, 10).endsAfter(Range.between(5, 10)));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_notOverlappingFirstSmaller() {
+ Range.between(0, 10).combineWith(Range.between(11, 20));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_notOverlappingSecondLarger() {
+ Range.between(11, 20).combineWith(Range.between(0, 10));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_firstEmptyNotOverlapping() {
+ Range.between(15, 15).combineWith(Range.between(0, 10));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_secondEmptyNotOverlapping() {
+ Range.between(0, 10).combineWith(Range.between(15, 15));
+ }
+
+ @Test
+ public void combine_barelyOverlapping() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(10, 20);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(0, combined1.getStart());
+ assertEquals(20, combined1.getEnd());
+ }
+
+ @Test
+ public void combine_subRange() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(2, 8);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(r1, combined1);
+ }
+
+ @Test
+ public void combine_intersecting() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(5, 15);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(0, combined1.getStart());
+ assertEquals(15, combined1.getEnd());
+
+ }
+
+ @Test
+ public void combine_emptyInside() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(5, 5);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(r1, combined1);
+ }
+
+ @Test
+ public void expand_basic() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = r1.expand(2, 3);
+
+ assertEquals(Range.between(3, 13), r2);
+ }
+
+ @Test
+ public void expand_negativeLegal() {
+ Range r1 = Range.between(5, 10);
+
+ Range r2 = r1.expand(-2, -2);
+ assertEquals(Range.between(7, 8), r2);
+
+ Range r3 = r1.expand(-3, -2);
+ assertEquals(Range.between(8, 8), r3);
+
+ Range r4 = r1.expand(3, -8);
+ assertEquals(Range.between(2, 2), r4);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void expand_negativeIllegal1() {
+ Range r1 = Range.between(5, 10);
+
+ // Should throw because the start would contract beyond the end
+ r1.expand(-3, -3);
+
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void expand_negativeIllegal2() {
+ Range r1 = Range.between(5, 10);
+
+ // Should throw because the end would contract beyond the start
+ r1.expand(3, -9);
+ }
+
+ @Test
+ public void restrictTo_fullyInside() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(4, 11);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue(r1 == r3);
+ }
+
+ @Test
+ public void restrictTo_fullyOutside() {
+ Range r1 = Range.between(4, 11);
+ Range r2 = Range.between(5, 10);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue(r2 == r3);
+ }
+
+ @Test
+ public void restrictTo_notInterstecting() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(15, 20);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue("Non-intersecting ranges should produce an empty result",
+ r3.isEmpty());
+
+ Range r4 = r2.restrictTo(r1);
+ assertTrue("Non-intersecting ranges should produce an empty result",
+ r4.isEmpty());
+ }
+
+ @Test
+ public void restrictTo_startOutside() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(7, 15);
+
+ Range r3 = r1.restrictTo(r2);
+
+ assertEquals(Range.between(7, 10), r3);
+
+ assertEquals(r2.restrictTo(r1), r3);
+ }
+
+ @Test
+ public void restrictTo_endOutside() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(4, 7);
+
+ Range r3 = r1.restrictTo(r2);
+
+ assertEquals(Range.between(5, 7), r3);
+
+ assertEquals(r2.restrictTo(r1), r3);
+ }
+
+ @Test
+ public void restrictTo_empty() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(0, 0);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue(r3.isEmpty());
+
+ Range r4 = r2.restrictTo(r1);
+ assertTrue(r4.isEmpty());
+ }
+
+}
diff --git a/shared/tests/src/com/vaadin/shared/util/SharedUtilTests.java b/shared/tests/src/com/vaadin/shared/util/SharedUtilTests.java
index b593032bd6..208d4ca7c7 100644
--- a/shared/tests/src/com/vaadin/shared/util/SharedUtilTests.java
+++ b/shared/tests/src/com/vaadin/shared/util/SharedUtilTests.java
@@ -1,43 +1,79 @@
package com.vaadin.shared.util;
-import org.junit.Before;
-import org.junit.Test;
-
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
-public class SharedUtilTests {
-
- private SharedUtil sut;
+import org.junit.Assert;
+import org.junit.Test;
- @Before
- public void setup() {
- sut = new SharedUtil();
- }
+public class SharedUtilTests {
@Test
public void trailingSlashIsTrimmed() {
- assertThat(sut.trimTrailingSlashes("/path/"), is("/path"));
+ assertThat(SharedUtil.trimTrailingSlashes("/path/"), is("/path"));
}
@Test
public void noTrailingSlashForTrimming() {
- assertThat(sut.trimTrailingSlashes("/path"), is("/path"));
+ assertThat(SharedUtil.trimTrailingSlashes("/path"), is("/path"));
}
@Test
public void trailingSlashesAreTrimmed() {
- assertThat(sut.trimTrailingSlashes("/path///"), is("/path"));
+ assertThat(SharedUtil.trimTrailingSlashes("/path///"), is("/path"));
}
@Test
public void emptyStringIsHandled() {
- assertThat(sut.trimTrailingSlashes(""), is(""));
+ assertThat(SharedUtil.trimTrailingSlashes(""), is(""));
}
@Test
public void rootSlashIsTrimmed() {
- assertThat(sut.trimTrailingSlashes("/"), is(""));
+ assertThat(SharedUtil.trimTrailingSlashes("/"), is(""));
}
+ @Test
+ public void camelCaseToHumanReadable() {
+ Assert.assertEquals("First Name",
+ SharedUtil.camelCaseToHumanFriendly("firstName"));
+ Assert.assertEquals("First Name",
+ SharedUtil.camelCaseToHumanFriendly("first name"));
+ Assert.assertEquals("First Name2",
+ SharedUtil.camelCaseToHumanFriendly("firstName2"));
+ Assert.assertEquals("First",
+ SharedUtil.camelCaseToHumanFriendly("first"));
+ Assert.assertEquals("First",
+ SharedUtil.camelCaseToHumanFriendly("First"));
+ Assert.assertEquals("Some XYZ Abbreviation",
+ SharedUtil.camelCaseToHumanFriendly("SomeXYZAbbreviation"));
+
+ // Javadoc examples
+ Assert.assertEquals("My Bean Container",
+ SharedUtil.camelCaseToHumanFriendly("MyBeanContainer"));
+ Assert.assertEquals("Awesome URL Factory",
+ SharedUtil.camelCaseToHumanFriendly("AwesomeURLFactory"));
+ Assert.assertEquals("Some Uri Action",
+ SharedUtil.camelCaseToHumanFriendly("SomeUriAction"));
+
+ }
+
+ @Test
+ public void splitCamelCase() {
+ assertCamelCaseSplit("firstName", "first", "Name");
+ assertCamelCaseSplit("fooBar", "foo", "Bar");
+ assertCamelCaseSplit("fooBar", "foo", "Bar");
+ assertCamelCaseSplit("fBar", "f", "Bar");
+ assertCamelCaseSplit("FBar", "F", "Bar");
+ assertCamelCaseSplit("MYCdi", "MY", "Cdi");
+ assertCamelCaseSplit("MyCDIUI", "My", "CDIUI");
+ assertCamelCaseSplit("MyCDIUITwo", "My", "CDIUI", "Two");
+ assertCamelCaseSplit("first name", "first", "name");
+
+ }
+
+ private void assertCamelCaseSplit(String camelCaseString, String... parts) {
+ String[] splitParts = SharedUtil.splitCamelCase(camelCaseString);
+ Assert.assertArrayEquals(parts, splitParts);
+ }
}
diff --git a/themes/build.xml b/themes/build.xml
index 4a95c043fc..487376ebdf 100644
--- a/themes/build.xml
+++ b/themes/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-themes" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-themes" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
Themes compiled to CSS
</description>
@@ -24,8 +25,10 @@
</union>
<target name="compile-themes">
- <ivy:resolve log="download-only" resolveid="common" conf="build" />
- <ivy:cachepath pathid="classpath.compile.theme" conf="build" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="build" />
+ <ivy:cachepath pathid="classpath.compile.theme"
+ conf="build" />
<antcall target="compile-theme">
<param name="theme" value="base" />
@@ -55,7 +58,8 @@
</target>
<target name="copy-theme">
- <fail unless="theme" message="You must give the theme name to copy n the 'theme' parameter" />
+ <fail unless="theme"
+ message="You must give the theme name to copy n the 'theme' parameter" />
<property name="theme.source.dir" location="../WebContent/VAADIN/themes/${theme}/" />
<copy todir="${theme.result.dir}/${theme}">
@@ -72,17 +76,23 @@
</target>
<target name="compile-theme" depends="copy-theme">
- <fail unless="theme" message="You must give the theme name to compile in the 'theme' parameter" />
+ <fail unless="theme"
+ message="You must give the theme name to compile in the 'theme' parameter" />
- <ivy:resolve log="download-only" resolveid="common" conf="compile-theme" />
- <ivy:cachepath pathid="classpath.compile.theme" conf="compile-theme" />
- <ivy:cachepath pathid="classpath.runtime.theme" conf="build" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="compile-theme" />
+ <ivy:cachepath pathid="classpath.compile.theme"
+ conf="compile-theme" />
+ <ivy:cachepath pathid="classpath.runtime.theme"
+ conf="build" />
<echo>Compiling ${theme}</echo>
<mkdir dir="${theme.result.dir}" />
<!-- compile the theme -->
- <java classname="com.vaadin.buildhelpers.CompileTheme" classpathref="classpath.compile.theme" failonerror="yes" fork="yes" maxmemory="512m">
+ <java classname="com.vaadin.buildhelpers.CompileTheme"
+ classpathref="classpath.compile.theme" failonerror="yes"
+ fork="yes" maxmemory="512m">
<arg value="--theme" />
<arg value="${theme}" />
<arg value="--theme-folder" />
diff --git a/uitest/build.xml b/uitest/build.xml
index 02b97fb3a0..e6c68a90aa 100644
--- a/uitest/build.xml
+++ b/uitest/build.xml
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
-<project name="vaadin-uitest" basedir="." default="publish-local" xmlns:ivy="antlib:org.apache.ivy.ant">
+<project name="vaadin-uitest" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
<description>
Provides a uitest WAR containing Vaadin UI tests
</description>
@@ -12,20 +13,24 @@
<property name="uitest.dir" location="${vaadin.basedir}/uitest" />
<property name="result.dir" value="result" />
<property name="theme.result.dir" value="${result.dir}/VAADIN/themes" />
- <property name="result.war" location="${result.dir}/lib/${module.name}-${vaadin.version}.war" />
+ <property name="result.war"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.war" />
<path id="classpath.compile.custom">
</path>
<target name="dependencies">
- <!-- This is copied from common.xml to be able to add server.test.source
+ <!-- This is copied from common.xml to be able to add server.test.source
to the source path -->
- <ivy:resolve log="download-only" resolveid="common" conf="build, build-provided" />
- <ivy:cachepath pathid="classpath.compile.dependencies" conf="build, build-provided" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="build, build-provided" />
+ <ivy:cachepath pathid="classpath.compile.dependencies"
+ conf="build, build-provided" />
</target>
- <target name="compile" description="Compiles the module" depends="dependencies">
+ <target name="compile" description="Compiles the module"
+ depends="dependencies">
<fail unless="module.name" message="No module name given" />
<property name="result.dir" location="result" />
@@ -35,16 +40,21 @@
<mkdir dir="${classes}" />
<!-- TODO: Get rid of this -->
- <javac destdir="${classes}" source="${vaadin.java.version}" target="${vaadin.java.version}" debug="true" encoding="UTF-8" includeantruntime="false">
+ <javac destdir="${classes}" source="${vaadin.java.version}"
+ target="${vaadin.java.version}" debug="true" encoding="UTF-8"
+ includeantruntime="false">
<src path="${server.test.sources}" />
<include name="com/vaadin/tests/data/bean/**" />
<include name="com/vaadin/tests/VaadinClasses.java" />
- <include name="com/vaadin/data/util/sqlcontainer/SQLTestsConstants.java" />
+ <include
+ name="com/vaadin/data/util/sqlcontainer/SQLTestsConstants.java" />
<classpath refid="classpath.compile.dependencies" />
<classpath refid="classpath.compile.custom" />
</javac>
- <javac destdir="${classes}" source="${vaadin.java.version}" target="${vaadin.java.version}" debug="true" encoding="UTF-8" includeantruntime="false">
+ <javac destdir="${classes}" source="${vaadin.java.version}"
+ target="${vaadin.java.version}" debug="true" encoding="UTF-8"
+ includeantruntime="false">
<src path="${src}" />
<classpath location="${classes}" />
<classpath refid="classpath.compile.dependencies" />
@@ -53,7 +63,8 @@
</target>
<target name="testing-widgetset" depends="dependencies,compile">
- <property name="module" value="com.vaadin.tests.widgetset.TestingWidgetSet" />
+ <property name="module"
+ value="com.vaadin.tests.widgetset.TestingWidgetSet" />
<property name="style" value="OBF" />
<property name="localWorkers" value="6" />
<property name="extraParams" value="" />
@@ -65,7 +76,8 @@
<echo>Compiling ${module} to ${module.output.dir}</echo>
<!-- compile the module -->
- <java classname="com.google.gwt.dev.Compiler" classpathref="classpath.compile.dependencies" failonerror="yes" fork="yes" maxmemory="512m">
+ <java classname="com.google.gwt.dev.Compiler" classpathref="classpath.compile.dependencies"
+ failonerror="yes" fork="yes" maxmemory="512m">
<classpath location="src" />
<classpath location="${classes}" />
<arg value="-workDir" />
@@ -92,15 +104,18 @@
</target>
- <target name="war" depends="dependencies, compile, compile-test-themes, testing-widgetset">
+ <target name="war"
+ depends="dependencies, compile, compile-test-themes, testing-widgetset">
<property name="result.dir" location="result" />
<property name="classes" location="${result.dir}/classes" />
<property name="WebContent.dir" location="${vaadin.basedir}/WebContent" />
<property name="deps.dir" location="${result.dir}/deps" />
<property name="src" location="${result.dir}/../src" />
- <ivy:resolve log="download-only" resolveid="common" conf="build" />
- <ivy:cachepath pathid="classpath.runtime.dependencies" conf="build" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="build" />
+ <ivy:cachepath pathid="classpath.runtime.dependencies"
+ conf="build" />
<delete dir="${deps.dir}" />
<mkdir dir="${deps.dir}" />
@@ -110,16 +125,14 @@
</copy>
<delete>
- <!-- Avoid including some potentially conflicting jars in the war -->
+ <!-- Avoid including some potentially conflicting jars in the
+ war -->
<fileset dir="${deps.dir}" includes="jetty-*.jar" />
<fileset dir="${deps.dir}" includes="servlet-api-*.jar" />
</delete>
- <!-- Ensure filtered webcontent files are available -->
- <antcall target="common.filter.webcontent" />
-
<war destfile="${result.war}" duplicate="fail" index="true">
- <fileset refid="common.files.for.all.jars" />
+ <fileset dir="${common.jarfiles.dir}" />
<fileset dir="${result.dir}">
<include name="VAADIN/widgetsets/**/*" />
<include name="VAADIN/themes/tests-valo*/**" />
@@ -156,38 +169,45 @@
<target name="test" depends="checkstyle">
</target>
- <target name="test-testbench" depends="clean-testbench-errors" description="Run all TestBench based tests, including server tests">
+ <target name="test-testbench" depends="clean-testbench-errors"
+ description="Run all TestBench based tests, including server tests">
<parallel>
<daemons>
<!-- Start server -->
- <ant antfile="${uitest.dir}/vaadin-server.xml" inheritall="true" inheritrefs="true" target="deploy-and-start" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ inheritall="true" inheritrefs="true" target="deploy-and-start" />
</daemons>
<sequential>
<!-- Server tests -->
- <!-- Sleep before running integration tests so testbench 2
- tests have time to compile and start -->
+ <!-- Sleep before running integration tests so testbench
+ 2 tests have time to compile and start -->
<sleep minutes="4" />
- <ant antfile="${uitest.dir}/integration_tests.xml" target="integration-test-all" inheritall="false" inheritrefs="false">
+ <ant antfile="${uitest.dir}/integration_tests.xml"
+ target="integration-test-all" inheritall="false"
+ inheritrefs="false">
<property name="demo.war" value="${war.file}" />
</ant>
</sequential>
<sequential>
<!-- Wait for server to start -->
- <ant antfile="${uitest.dir}/vaadin-server.xml" target="wait-for-startup" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ target="wait-for-startup" />
<!-- Run all different kinds of TestBench tests in parallel -->
<parallel>
<!-- Legacy TestBench 2 tests -->
<sequential>
- <ant antfile="${uitest.dir}/test.xml" target="tb2-tests" />
+ <ant antfile="${uitest.dir}/test.xml"
+ target="tb2-tests" />
<echo message="TestBench 2 tests complete" />
</sequential>
<!-- TestBench 3 tests -->
<sequential>
- <ant antfile="${uitest.dir}/tb3test.xml" target="run-all-tb3-tests" inheritall="true" />
+ <ant antfile="${uitest.dir}/tb3test.xml"
+ target="run-all-tb3-tests" inheritall="true" />
<echo message="TestBench 3 tests complete" />
</sequential>
</parallel>
@@ -196,13 +216,17 @@
</target>
<target name="test-server" depends="clean-testbench-errors">
- <property name="war.file" location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
+ <property name="war.file"
+ location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
<parallel>
<daemons>
- <ant antfile="${uitest.dir}/vaadin-server.xml" inheritall="true" inheritrefs="true" target="deploy-and-start" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ inheritall="true" inheritrefs="true" target="deploy-and-start" />
</daemons>
<sequential>
- <ant antfile="${uitest.dir}/integration_tests.xml" target="integration-test-all" inheritall="false" inheritrefs="false">
+ <ant antfile="${uitest.dir}/integration_tests.xml"
+ target="integration-test-all" inheritall="false"
+ inheritrefs="false">
<property name="demo.war" value="${war.file}" />
</ant>
</sequential>
@@ -210,36 +234,45 @@
</target>
<target name="test-tb2" depends="clean-testbench-errors">
- <property name="war.file" location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
+ <property name="war.file"
+ location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
<parallel>
<daemons>
- <ant antfile="${uitest.dir}/vaadin-server.xml" inheritall="true" inheritrefs="true" target="deploy-and-start" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ inheritall="true" inheritrefs="true" target="deploy-and-start" />
</daemons>
<sequential>
- <ant antfile="${uitest.dir}/vaadin-server.xml" target="wait-for-startup" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ target="wait-for-startup" />
<ant antfile="${uitest.dir}/test.xml" target="tb2-tests" />
</sequential>
</parallel>
</target>
<target name="test-tb3" depends="clean-testbench-errors">
- <property name="war.file" location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
+ <property name="war.file"
+ location="${vaadin.basedir}/result/artifacts/${vaadin.version}/vaadin-uitest/vaadin-uitest-${vaadin.version}.war" />
<parallel>
<daemons>
- <ant antfile="${uitest.dir}/vaadin-server.xml" inheritall="true" inheritrefs="true" target="deploy-and-start" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ inheritall="true" inheritrefs="true" target="deploy-and-start" />
</daemons>
<sequential>
- <ant antfile="${uitest.dir}/vaadin-server.xml" target="wait-for-startup" />
- <ant antfile="${uitest.dir}/tb3test.xml" target="run-all-tb3-tests" inheritall="true" />
+ <ant antfile="${uitest.dir}/vaadin-server.xml"
+ target="wait-for-startup" />
+ <ant antfile="${uitest.dir}/tb3test.xml" target="run-all-tb3-tests"
+ inheritall="true" />
</sequential>
</parallel>
</target>
<target name="clean-testbench-errors">
- <fail unless="com.vaadin.testbench.screenshot.directory" message="Define screenshot directory using -Dcom.vaadin.testbench.screenshot.directory" />
+ <fail unless="com.vaadin.testbench.screenshot.directory"
+ message="Define screenshot directory using -Dcom.vaadin.testbench.screenshot.directory" />
<mkdir dir="${com.vaadin.testbench.screenshot.directory}/errors" />
<delete>
- <fileset dir="${com.vaadin.testbench.screenshot.directory}/errors">
+ <fileset
+ dir="${com.vaadin.testbench.screenshot.directory}/errors">
<include name="*" />
</fileset>
</delete>
@@ -268,22 +301,28 @@
<param name="theme" value="tests-valo-blueprint" />
</antcall>
<antcall target="compile-theme">
- <param name="theme" value="tests-valo-light" />
+ <param name="theme" value="tests-valo-light" />
</antcall>
</target>
<target name="compile-theme" depends="copy-theme">
- <fail unless="theme" message="You must give the theme name to compile in the 'theme' parameter" />
+ <fail unless="theme"
+ message="You must give the theme name to compile in the 'theme' parameter" />
- <ivy:resolve log="download-only" resolveid="common" conf="compile-theme" />
- <ivy:cachepath pathid="classpath.compile.theme" conf="compile-theme" />
- <ivy:cachepath pathid="classpath.runtime.theme" conf="build" />
+ <ivy:resolve log="download-only" resolveid="common"
+ conf="compile-theme" />
+ <ivy:cachepath pathid="classpath.compile.theme"
+ conf="compile-theme" />
+ <ivy:cachepath pathid="classpath.runtime.theme"
+ conf="build" />
<echo>Compiling ${theme}</echo>
<mkdir dir="${theme.result.dir}" />
<!-- compile the theme -->
- <java classname="com.vaadin.buildhelpers.CompileTheme" classpathref="classpath.compile.theme" failonerror="yes" fork="yes" maxmemory="512m">
+ <java classname="com.vaadin.buildhelpers.CompileTheme"
+ classpathref="classpath.compile.theme" failonerror="yes"
+ fork="yes" maxmemory="512m">
<arg value="--theme" />
<arg value="${theme}" />
<arg value="--theme-folder" />
@@ -298,7 +337,8 @@
</target>
<target name="copy-theme">
- <fail unless="theme" message="You must give the theme name to copy n the 'theme' parameter" />
+ <fail unless="theme"
+ message="You must give the theme name to copy n the 'theme' parameter" />
<property name="theme.source.dir" location="../WebContent/VAADIN/themes" />
<copy todir="${theme.result.dir}">
diff --git a/uitest/src/com/vaadin/testbench/elements/GridElement.java b/uitest/src/com/vaadin/testbench/elements/GridElement.java
new file mode 100644
index 0000000000..0c94c1dd88
--- /dev/null
+++ b/uitest/src/com/vaadin/testbench/elements/GridElement.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.testbench.elements;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+
+/**
+ * TestBench Element API for Grid
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@ServerClass("com.vaadin.ui.Grid")
+public class GridElement extends AbstractComponentElement {
+
+ public static class GridCellElement extends AbstractElement {
+
+ private static final String FOCUSED_CELL_CLASS_NAME = "-cell-focused";
+ private static final String FROZEN_CLASS_NAME = "frozen";
+
+ public boolean isFocused() {
+ return getAttribute("class").contains(FOCUSED_CELL_CLASS_NAME);
+ }
+
+ public boolean isFrozen() {
+ return getAttribute("class").contains(FROZEN_CLASS_NAME);
+ }
+ }
+
+ public static class GridRowElement extends AbstractElement {
+
+ private static final String FOCUSED_CLASS_NAME = "-row-focused";
+ private static final String SELECTED_CLASS_NAME = "-row-selected";
+
+ public boolean isFocused() {
+ return getAttribute("class").contains(FOCUSED_CLASS_NAME);
+ }
+
+ @Override
+ public boolean isSelected() {
+ return getAttribute("class").contains(SELECTED_CLASS_NAME);
+ }
+ }
+
+ public static class GridEditorElement extends AbstractElement {
+
+ private GridElement grid;
+
+ private GridEditorElement setGrid(GridElement grid) {
+ this.grid = grid;
+ return this;
+ }
+
+ /**
+ * Gets the editor field for column in given index.
+ *
+ * @param colIndex
+ * column index
+ * @return the editor field for given location
+ */
+ public TestBenchElement getField(int colIndex) {
+ return grid.getSubPart("#editor[" + colIndex + "]");
+ }
+
+ /**
+ * Saves the fields of this editor.
+ * <p>
+ * <em>Note:</em> that this closes the editor making this element
+ * useless.
+ */
+ public void save() {
+ getField(0);
+ List<WebElement> buttons = findElements(By.xpath("./button"));
+ buttons.get(0).click();
+ }
+
+ /**
+ * Cancels this editor.
+ * <p>
+ * <em>Note:</em> that this closes the editor making this element
+ * useless.
+ */
+ public void cancel() {
+ getField(0);
+ List<WebElement> buttons = findElements(By.xpath("./button"));
+ buttons.get(1).click();
+ }
+ }
+
+ /**
+ * Scrolls Grid element so that wanted row is displayed
+ *
+ * @param index
+ * Target row
+ */
+ public void scrollToRow(int index) {
+ try {
+ getSubPart("#cell[" + index + "]");
+ } catch (NoSuchElementException e) {
+ // Expected, ignore it.
+ }
+ }
+
+ /**
+ * Gets cell element with given row and column index.
+ *
+ * @param rowIndex
+ * Row index
+ * @param colIndex
+ * Column index
+ * @return Cell element with given indices.
+ */
+ public GridCellElement getCell(int rowIndex, int colIndex) {
+ scrollToRow(rowIndex);
+ return getSubPart("#cell[" + rowIndex + "][" + colIndex + "]").wrap(
+ GridCellElement.class);
+ }
+
+ /**
+ * Gets row element with given row index.
+ *
+ * @param index
+ * Row index
+ * @return Row element with given index.
+ */
+ public GridRowElement getRow(int index) {
+ scrollToRow(index);
+ return getSubPart("#cell[" + index + "]").wrap(GridRowElement.class);
+ }
+
+ /**
+ * Gets header cell element with given row and column index.
+ *
+ * @param rowIndex
+ * Row index
+ * @param colIndex
+ * Column index
+ * @return Header cell element with given indices.
+ */
+ public GridCellElement getHeaderCell(int rowIndex, int colIndex) {
+ return getSubPart("#header[" + rowIndex + "][" + colIndex + "]").wrap(
+ GridCellElement.class);
+ }
+
+ /**
+ * Gets footer cell element with given row and column index.
+ *
+ * @param rowIndex
+ * Row index
+ * @param colIndex
+ * Column index
+ * @return Footer cell element with given indices.
+ */
+ public GridCellElement getFooterCell(int rowIndex, int colIndex) {
+ return getSubPart("#footer[" + rowIndex + "][" + colIndex + "]").wrap(
+ GridCellElement.class);
+ }
+
+ /**
+ * Gets list of header cell elements on given row.
+ *
+ * @param rowIndex
+ * Row index
+ * @return Header cell elements on given row.
+ */
+ public List<GridCellElement> getHeaderCells(int rowIndex) {
+ List<GridCellElement> headers = new ArrayList<GridCellElement>();
+ for (TestBenchElement e : TestBenchElement.wrapElements(
+ getSubPart("#header[" + rowIndex + "]").findElements(
+ By.xpath("./th")), getCommandExecutor())) {
+ headers.add(e.wrap(GridCellElement.class));
+ }
+ return headers;
+ }
+
+ /**
+ * Gets list of header cell elements on given row.
+ *
+ * @param rowIndex
+ * Row index
+ * @return Header cell elements on given row.
+ */
+ public List<GridCellElement> getFooterCells(int rowIndex) {
+ List<GridCellElement> footers = new ArrayList<GridCellElement>();
+ for (TestBenchElement e : TestBenchElement.wrapElements(
+ getSubPart("#footer[" + rowIndex + "]").findElements(
+ By.xpath("./td")), getCommandExecutor())) {
+ footers.add(e.wrap(GridCellElement.class));
+ }
+ return footers;
+ }
+
+ /**
+ * Get header row count
+ *
+ * @return Header row count
+ */
+ public int getHeaderCount() {
+ return getSubPart("#header").findElements(By.xpath("./tr")).size();
+ }
+
+ /**
+ * Get footer row count
+ *
+ * @return Footer row count
+ */
+ public int getFooterCount() {
+ return getSubPart("#footer").findElements(By.xpath("./tr")).size();
+ }
+
+ /**
+ * Get a header row by index
+ *
+ * @param rowIndex
+ * Row index
+ * @return The th element of the row
+ */
+ public WebElement getHeaderRow(int rowIndex) {
+ return getSubPart("#header[" + rowIndex + "]");
+ }
+
+ /**
+ * Get a footer row by index
+ *
+ * @param rowIndex
+ * Row index
+ * @return The tr element of the row
+ */
+ public WebElement getFooterRow(int rowIndex) {
+ return getSubPart("#footer[" + rowIndex + "]");
+ }
+
+ /**
+ * Get the vertical scroll element
+ *
+ * @return The element representing the vertical scrollbar
+ */
+ public WebElement getVerticalScroller() {
+ List<WebElement> rootElements = findElements(By.xpath("./div"));
+ return rootElements.get(0);
+ }
+
+ /**
+ * Get the horizontal scroll element
+ *
+ * @return The element representing the horizontal scrollbar
+ */
+ public WebElement getHorizontalScroller() {
+ List<WebElement> rootElements = findElements(By.xpath("./div"));
+ return rootElements.get(1);
+ }
+
+ /**
+ * Get the header element
+ *
+ * @return The thead element
+ */
+ public WebElement getHeader() {
+ return getSubPart("#header");
+ }
+
+ /**
+ * Get the body element
+ *
+ * @return the tbody element
+ */
+ public WebElement getBody() {
+ return getSubPart("#cell");
+ }
+
+ /**
+ * Get the footer element
+ *
+ * @return the tfoot element
+ */
+ public WebElement getFooter() {
+ return getSubPart("#footer");
+ }
+
+ /**
+ * Get the element wrapping the table element
+ *
+ * @return The element that wraps the table element
+ */
+ public WebElement getTableWrapper() {
+ List<WebElement> rootElements = findElements(By.xpath("./div"));
+ return rootElements.get(2);
+ }
+
+ public GridEditorElement getEditor() {
+ return getSubPart("#editor").wrap(GridEditorElement.class)
+ .setGrid(this);
+ }
+
+ /**
+ * Helper function to get Grid subparts wrapped correctly
+ *
+ * @param subPartSelector
+ * SubPart to be used in ComponentLocator
+ * @return SubPart element wrapped in TestBenchElement class
+ */
+ private TestBenchElement getSubPart(String subPartSelector) {
+ return (TestBenchElement) findElement(By.vaadin(subPartSelector));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/AbstractGridColumnAutoWidthTest.java b/uitest/src/com/vaadin/tests/components/grid/AbstractGridColumnAutoWidthTest.java
new file mode 100644
index 0000000000..cc5be455cd
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/AbstractGridColumnAutoWidthTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@SuppressWarnings("boxing")
+@TestCategory("grid")
+public abstract class AbstractGridColumnAutoWidthTest extends MultiBrowserTest {
+
+ public static final int TOTAL_MARGIN_PX = 13;
+
+ @Before
+ public void before() {
+ openTestURL();
+ }
+
+ @Test
+ public void testNarrowHeaderWideBody() {
+ WebElement[] col = getColumn(1);
+ int headerWidth = col[0].getSize().getWidth();
+ int bodyWidth = col[1].getSize().getWidth();
+ int colWidth = col[2].getSize().getWidth() - TOTAL_MARGIN_PX;
+
+ assertLessThan("header should've been narrower than body", headerWidth,
+ bodyWidth);
+ assertEquals("column should've been roughly as wide as the body",
+ bodyWidth, colWidth, 5);
+ }
+
+ @Test
+ public void testWideHeaderNarrowBody() {
+ WebElement[] col = getColumn(2);
+ int headerWidth = col[0].getSize().getWidth();
+ int bodyWidth = col[1].getSize().getWidth();
+ int colWidth = col[2].getSize().getWidth() - TOTAL_MARGIN_PX;
+
+ assertGreater("header should've been wider than body", headerWidth,
+ bodyWidth);
+ assertEquals("column should've been roughly as wide as the header",
+ headerWidth, colWidth, 5);
+
+ }
+
+ @Test
+ public void testTooNarrowColumn() {
+ if (BrowserUtil.isIE(getDesiredCapabilities())) {
+ // IE can't deal with overflow nicely.
+ return;
+ }
+
+ WebElement[] col = getColumn(3);
+ int headerWidth = col[0].getSize().getWidth();
+ int colWidth = col[2].getSize().getWidth() - TOTAL_MARGIN_PX;
+
+ assertLessThan("column should've been narrower than content", colWidth,
+ headerWidth);
+ }
+
+ @Test
+ public void testTooWideColumn() {
+ WebElement[] col = getColumn(4);
+ int headerWidth = col[0].getSize().getWidth();
+ int colWidth = col[2].getSize().getWidth() - TOTAL_MARGIN_PX;
+
+ assertGreater("column should've been wider than content", colWidth,
+ headerWidth);
+ }
+
+ @Test
+ public void testColumnsRenderCorrectly() throws IOException {
+ compareScreen("initialRender");
+ }
+
+ private WebElement[] getColumn(int i) {
+ WebElement[] col = new WebElement[3];
+ col[0] = getDriver().findElement(
+ By.xpath("//thead//th[" + (i + 1) + "]/span"));
+ col[1] = getDriver().findElement(
+ By.xpath("//tbody//td[" + (i + 1) + "]/span"));
+ col[2] = getDriver().findElement(
+ By.xpath("//tbody//td[" + (i + 1) + "]"));
+ return col;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java b/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java
new file mode 100644
index 0000000000..f7d14bbff6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.Label;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class CustomRenderer extends AbstractTestUI {
+
+ private static final Object INT_ARRAY_PROPERTY = "int array";
+ private static final Object VOID_PROPERTY = "void";
+
+ static final Object ITEM_ID = "itemId1";
+ static final String DEBUG_LABEL_ID = "debuglabel";
+ static final String INIT_DEBUG_LABEL_CAPTION = "Debug label placeholder";
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty(INT_ARRAY_PROPERTY, int[].class,
+ new int[] {});
+ container.addContainerProperty(VOID_PROPERTY, Void.class, null);
+
+ Item item = container.addItem(ITEM_ID);
+
+ @SuppressWarnings("unchecked")
+ Property<int[]> propertyIntArray = item
+ .getItemProperty(INT_ARRAY_PROPERTY);
+ propertyIntArray.setValue(new int[] { 1, 1, 2, 3, 5, 8, 13 });
+
+ Label debugLabel = new Label(INIT_DEBUG_LABEL_CAPTION);
+ debugLabel.setId(DEBUG_LABEL_ID);
+
+ Grid grid = new Grid(container);
+ grid.getColumn(INT_ARRAY_PROPERTY).setRenderer(new IntArrayRenderer());
+ grid.getColumn(VOID_PROPERTY).setRenderer(
+ new RowAwareRenderer(debugLabel));
+ grid.setSelectionMode(SelectionMode.NONE);
+ addComponent(grid);
+ addComponent(debugLabel);
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Verifies that renderers operating on other data than "
+ + "just Strings also work ";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return Integer.valueOf(13334);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java b/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java
new file mode 100644
index 0000000000..1c00574f9c
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.LabelElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class CustomRendererTest extends MultiBrowserTest {
+ @Test
+ public void testIntArrayIsRendered() throws Exception {
+ openTestURL();
+
+ GridElement grid = findGrid();
+ assertEquals("1 :: 1 :: 2 :: 3 :: 5 :: 8 :: 13", grid.getCell(0, 0)
+ .getText());
+ }
+
+ @Test
+ public void testRowAwareRenderer() throws Exception {
+ openTestURL();
+
+ GridElement grid = findGrid();
+ assertEquals("Click me!", grid.getCell(0, 1).getText());
+ assertEquals(CustomRenderer.INIT_DEBUG_LABEL_CAPTION, findDebugLabel()
+ .getText());
+
+ grid.getCell(0, 1).click();
+ assertEquals("row: 0, key: 0", grid.getCell(0, 1).getText());
+ assertEquals("key: 0, itemId: " + CustomRenderer.ITEM_ID,
+ findDebugLabel().getText());
+ }
+
+ private GridElement findGrid() {
+ List<GridElement> elements = $(GridElement.class).all();
+ return elements.get(0);
+ }
+
+ private LabelElement findDebugLabel() {
+ return $(LabelElement.class).id(CustomRenderer.DEBUG_LABEL_ID);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInit.java b/uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInit.java
new file mode 100644
index 0000000000..36d92d79a0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInit.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+
+public class GridAddAndRemoveDataOnInit extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid gridAdd = new Grid();
+ gridAdd.setHeight("240px");
+ gridAdd.setWidth("140px");
+ addComponent(gridAdd);
+ Indexed dataSource = gridAdd.getContainerDataSource();
+ dataSource.addContainerProperty("foo", Integer.class, 0);
+ for (int i = 0; i < 10; ++i) {
+ Object id = dataSource.addItem();
+ dataSource.getItem(id).getItemProperty("foo").setValue(i);
+ }
+ dataSource = new IndexedContainer();
+ dataSource.addContainerProperty("bar", Integer.class, 0);
+ for (int i = 0; i < 10; ++i) {
+ Object id = dataSource.addItem();
+ dataSource.getItem(id).getItemProperty("bar").setValue(i);
+ }
+ Grid gridRemove = new Grid(dataSource);
+ gridRemove.setHeight("150px");
+ gridRemove.setWidth("140px");
+ addComponent(gridRemove);
+ for (int i = 0; i < 5; ++i) {
+ dataSource.removeItem(dataSource.getIdByIndex(i));
+ }
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Foo";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 13334;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInitTest.java b/uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInitTest.java
new file mode 100644
index 0000000000..ceaceb661d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridAddAndRemoveDataOnInitTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridAddAndRemoveDataOnInitTest extends MultiBrowserTest {
+
+ @Test
+ public void verifyGridSizes() {
+ openTestURL();
+
+ GridElement gridAdd = $(GridElement.class).first();
+ if (!gridAdd.isElementPresent(By.vaadin("#cell[9][0]"))
+ || gridAdd.isElementPresent(By.vaadin("#cell[10][0]"))) {
+ Assert.fail("Grid with added data contained incorrect rows");
+ }
+
+ GridElement gridRemove = $(GridElement.class).get(1);
+ if (!gridRemove.isElementPresent(By.vaadin("#cell[4][0]"))
+ || gridRemove.isElementPresent(By.vaadin("#cell[5][0]"))) {
+ Assert.fail("Grid with removed data contained incorrect rows");
+ }
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridAddRow.java b/uitest/src/com/vaadin/tests/components/grid/GridAddRow.java
new file mode 100644
index 0000000000..fa2d7b5399
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridAddRow.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class GridAddRow extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+
+ final Grid grid = new Grid();
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.addColumn("firstName");
+ grid.addColumn("age", Integer.class);
+
+ grid.addRow("Lorem", Integer.valueOf(1));
+ grid.addRow("Ipsum", Integer.valueOf(2));
+
+ addComponent(grid);
+
+ addComponent(new Button("Add new row", new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.addRow("Dolor", Integer.valueOf(3));
+ }
+ }));
+
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridAddRowTest.java b/uitest/src/com/vaadin/tests/components/grid/GridAddRowTest.java
new file mode 100644
index 0000000000..46f085686d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridAddRowTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridAddRowTest extends MultiBrowserTest {
+ @Test
+ public void testAddRow() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+
+ Assert.assertEquals("Lorem", grid.getCell(0, 1).getText());
+ Assert.assertEquals("2", grid.getCell(1, 2).getText());
+
+ addRow();
+
+ Assert.assertEquals("Dolor", grid.getCell(2, 1).getText());
+
+ addRow();
+
+ Assert.assertEquals("Dolor", grid.getCell(3, 1).getText());
+ }
+
+ private void addRow() {
+ $(ButtonElement.class).caption("Add new row").first().click();
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
new file mode 100644
index 0000000000..00db02bef3
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.LabelElement;
+import com.vaadin.testbench.elements.NativeButtonElement;
+import com.vaadin.testbench.elements.NativeSelectElement;
+import com.vaadin.testbench.elements.ServerClass;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers;
+import com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers;
+
+/**
+ * Tests Grid client side renderers
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@TestCategory("grid")
+public class GridClientRenderers extends MultiBrowserTest {
+
+ private static final double SLEEP_MULTIPLIER = 1.2;
+ private int latency = 0;
+
+ @Override
+ protected Class<?> getUIClass() {
+ return GridClientColumnRenderers.class;
+ }
+
+ @Override
+ protected String getDeploymentPath() {
+ String path = super.getDeploymentPath();
+ if (latency > 0) {
+ path += (path.contains("?") ? "&" : "?") + "latency=" + latency;
+ }
+ return path;
+ }
+
+ @ServerClass("com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers.GridController")
+ public static class MyClientGridElement extends GridElement {
+ }
+
+ @Override
+ public void setup() throws Exception {
+ latency = 0; // reset
+ super.setup();
+ }
+
+ @Test
+ public void addWidgetRenderer() throws Exception {
+ openTestURL();
+
+ // Add widget renderer column
+ $(NativeSelectElement.class).first().selectByText(
+ Renderers.WIDGET_RENDERER.toString());
+ $(NativeButtonElement.class).caption("Add").first().click();
+
+ // Click the button in cell 1,1
+ TestBenchElement cell = getGrid().getCell(1, 2);
+ WebElement gwtButton = cell.findElement(By.tagName("button"));
+ gwtButton.click();
+
+ // Should be an alert visible
+ assertEquals("Button did not contain text \"Clicked\"", "Clicked",
+ gwtButton.getText());
+ }
+
+ @Test
+ public void detachAndAttachGrid() {
+ openTestURL();
+
+ // Add widget renderer column
+ $(NativeSelectElement.class).first().selectByText(
+ Renderers.WIDGET_RENDERER.toString());
+ $(NativeButtonElement.class).caption("Add").first().click();
+
+ // Detach and re-attach the Grid
+ $(NativeButtonElement.class).caption("DetachAttach").first().click();
+
+ // Click the button in cell 1,1
+ TestBenchElement cell = getGrid().getCell(1, 2);
+ WebElement gwtButton = cell.findElement(By.tagName("button"));
+ gwtButton.click();
+
+ // Should be an alert visible
+ assertEquals("Button did not contain text \"Clicked\"",
+ gwtButton.getText(), "Clicked");
+ }
+
+ @Test
+ public void rowsWithDataHasStyleName() throws Exception {
+
+ testBench().disableWaitForVaadin();
+
+ // Simulate network latency with 2000ms
+ latency = 2000;
+
+ openTestURL();
+
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ TestBenchElement row = getGrid().getRow(51);
+ String className = row.getAttribute("class");
+ assertFalse(
+ "Row should not yet contain style name v-grid-row-has-data",
+ className.contains("v-grid-row-has-data"));
+
+ // Wait for data to arrive
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ row = getGrid().getRow(51);
+ className = row.getAttribute("class");
+ assertTrue("Row should now contain style name v-grid-row-has-data",
+ className.contains("v-grid-row-has-data"));
+ }
+
+ @Test
+ public void complexRendererSetVisibleContent() throws Exception {
+
+ DesiredCapabilities desiredCapabilities = getDesiredCapabilities();
+
+ // Simulate network latency with 2000ms
+ latency = 2000;
+ if (BrowserUtil.isIE8(desiredCapabilities)) {
+ // IE8 is slower than other browsers. Bigger latency is needed for
+ // stability in this test.
+ latency = 3000;
+ }
+
+ // Chrome uses RGB instead of RGBA
+ String colorRed = "rgba(255, 0, 0, 1)";
+ String colorWhite = "rgba(255, 255, 255, 1)";
+ String colorDark = "rgba(239, 240, 241, 1)";
+ if (BrowserUtil.isChrome(desiredCapabilities)) {
+ colorRed = "rgb(255, 0, 0)";
+ colorWhite = "rgb(255, 255, 255)";
+ colorDark = "rgb(239, 240, 241)";
+ }
+
+ openTestURL();
+
+ getGrid();
+
+ testBench().disableWaitForVaadin();
+
+ // Test initial renderering with contentVisible = False
+ TestBenchElement cell = getGrid().getCell(51, 1);
+ String backgroundColor = cell.getCssValue("backgroundColor");
+ assertEquals("Background color was not red.", colorRed, backgroundColor);
+
+ // data arrives...
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ // Content becomes visible
+ cell = getGrid().getCell(51, 1);
+ backgroundColor = cell.getCssValue("backgroundColor");
+ assertNotEquals("Background color was red.", colorRed, backgroundColor);
+
+ // scroll down, new cells becomes contentVisible = False
+ getGrid().scrollToRow(60);
+
+ // Cell should be red (setContentVisible set cell red)
+ cell = getGrid().getCell(55, 1);
+ backgroundColor = cell.getCssValue("backgroundColor");
+ assertEquals("Background color was not red.", colorRed, backgroundColor);
+
+ // data arrives...
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ // Cell should no longer be red
+ backgroundColor = cell.getCssValue("backgroundColor");
+ assertTrue(
+ "Background color was not reset",
+ backgroundColor.equals(colorWhite)
+ || backgroundColor.equals(colorDark));
+ }
+
+ @Test
+ public void testSortingEvent() throws Exception {
+ openTestURL();
+
+ $(NativeButtonElement.class).caption("Trigger sorting event").first()
+ .click();
+
+ String consoleText = $(LabelElement.class).id("testDebugConsole")
+ .getText();
+
+ assertTrue("Console text as expected",
+ consoleText.contains("Columns: 1, order: Column 1: ASCENDING"));
+
+ }
+
+ @Test
+ public void testListSorter() throws Exception {
+ openTestURL();
+
+ $(NativeButtonElement.class).caption("Shuffle").first().click();
+
+ GridElement gridElem = $(MyClientGridElement.class).first();
+
+ // XXX: DANGER! We'll need to know how many rows the Grid has!
+ // XXX: Currently, this is impossible; hence the hardcoded value of 70.
+
+ boolean shuffled = false;
+ for (int i = 1, l = 70; i < l; ++i) {
+
+ String str_a = gridElem.getCell(i - 1, 0).getAttribute("innerHTML");
+ String str_b = gridElem.getCell(i, 0).getAttribute("innerHTML");
+
+ int value_a = Integer.parseInt(str_a);
+ int value_b = Integer.parseInt(str_b);
+
+ if (value_a > value_b) {
+ shuffled = true;
+ break;
+ }
+ }
+ assertTrue("Grid shuffled", shuffled);
+
+ $(NativeButtonElement.class).caption("Test sorting").first().click();
+
+ for (int i = 1, l = 70; i < l; ++i) {
+
+ String str_a = gridElem.getCell(i - 1, 0).getAttribute("innerHTML");
+ String str_b = gridElem.getCell(i, 0).getAttribute("innerHTML");
+
+ int value_a = Integer.parseInt(str_a);
+ int value_b = Integer.parseInt(str_b);
+
+ if (value_a > value_b) {
+ assertTrue("Grid sorted", false);
+ }
+ }
+ }
+
+ @Test
+ public void testComplexRendererOnActivate() {
+ openTestURL();
+
+ GridCellElement cell = getGrid().getCell(3, 1);
+ cell.click();
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ assertEquals("onActivate was not called on KeyDown Enter.",
+ "Activated!", cell.getText());
+
+ cell = getGrid().getCell(4, 1);
+ cell.click();
+ new Actions(getDriver()).moveToElement(cell).doubleClick().perform();
+ assertEquals("onActivate was not called on double click.",
+ "Activated!", cell.getText());
+ }
+
+ private GridElement getGrid() {
+ return $(MyClientGridElement.class).first();
+ }
+
+ private void addColumn(Renderers renderer) {
+ // Add widget renderer column
+ $(NativeSelectElement.class).first().selectByText(renderer.toString());
+ $(NativeButtonElement.class).caption("Add").first().click();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColspans.java b/uitest/src/com/vaadin/tests/components/grid/GridColspans.java
new file mode 100644
index 0000000000..80337971b6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColspans.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.FooterRow;
+import com.vaadin.ui.Grid.HeaderRow;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.renderer.NumberRenderer;
+
+public class GridColspans extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Indexed dataSource = new IndexedContainer();
+ final Grid grid;
+
+ dataSource.addContainerProperty("firstName", String.class, "");
+ dataSource.addContainerProperty("lastName", String.class, "");
+ dataSource.addContainerProperty("streetAddress", String.class, "");
+ dataSource.addContainerProperty("zipCode", Integer.class, null);
+ dataSource.addContainerProperty("city", String.class, "");
+ Item i = dataSource.addItem(0);
+ i.getItemProperty("firstName").setValue("Rudolph");
+ i.getItemProperty("lastName").setValue("Reindeer");
+ i.getItemProperty("streetAddress").setValue("Ruukinkatu 2-4");
+ i.getItemProperty("zipCode").setValue(20540);
+ i.getItemProperty("city").setValue("Turku");
+ grid = new Grid(dataSource);
+ grid.setWidth("600px");
+ grid.getColumn("zipCode").setRenderer(new NumberRenderer());
+ grid.setSelectionMode(SelectionMode.MULTI);
+ addComponent(grid);
+
+ HeaderRow row = grid.prependHeaderRow();
+ row.join("firstName", "lastName").setText("Full Name");
+ row.join("streetAddress", "zipCode", "city").setText("Address");
+ grid.prependHeaderRow()
+ .join(dataSource.getContainerPropertyIds().toArray())
+ .setText("All the stuff");
+
+ FooterRow footerRow = grid.appendFooterRow();
+ footerRow.join("firstName", "lastName").setText("Full Name");
+ footerRow.join("streetAddress", "zipCode", "city").setText("Address");
+ grid.appendFooterRow()
+ .join(dataSource.getContainerPropertyIds().toArray())
+ .setText("All the stuff");
+
+ addComponent(new Button("Show/Hide firstName",
+ new Button.ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ if (grid.getColumn("firstName") != null) {
+ grid.removeColumn("firstName");
+ } else {
+ grid.addColumn("firstName");
+ }
+ }
+ }));
+
+ addComponent(new Button("Change column order",
+ new Button.ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.setColumnOrder("zipCode", "firstName");
+ }
+ }));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Grid header and footer colspans";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 13334;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java b/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java
new file mode 100644
index 0000000000..6b50b64732
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridColspansTest extends MultiBrowserTest {
+
+ @Before
+ public void setUp() {
+ setDebug(true);
+ }
+
+ @Test
+ public void testHeaderColSpans() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("5", grid.getHeaderCell(0, 1).getAttribute("colspan"));
+ assertEquals("2", grid.getHeaderCell(1, 1).getAttribute("colspan"));
+ assertEquals("3", grid.getHeaderCell(1, 3).getAttribute("colspan"));
+ }
+
+ @Test
+ public void testFooterColSpans() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("5", grid.getFooterCell(1, 1).getAttribute("colspan"));
+ assertEquals("2", grid.getFooterCell(0, 1).getAttribute("colspan"));
+ assertEquals("3", grid.getFooterCell(0, 3).getAttribute("colspan"));
+ }
+
+ @Test
+ public void testHideFirstColumnOfColspan() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("Failed initial condition.", "all the stuff", grid
+ .getHeaderCell(0, 1).getText().toLowerCase());
+ assertEquals("Failed initial condition.", "first name", grid
+ .getHeaderCell(2, 1).getText().toLowerCase());
+ $(ButtonElement.class).caption("Show/Hide firstName").first().click();
+ assertEquals("Header text changed on column hide.", "all the stuff",
+ grid.getHeaderCell(0, 1).getText().toLowerCase());
+ assertEquals("Failed initial condition.", "last name", grid
+ .getHeaderCell(2, 1).getText().toLowerCase());
+ }
+
+ @Test
+ public void testSplittingMergedHeaders() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ GridCellElement headerCell = grid.getHeaderCell(1, 1);
+ assertEquals("Failed initial condition.", "full name", headerCell
+ .getText().toLowerCase());
+ assertEquals("Failed initial condition.", "first name", grid
+ .getHeaderCell(2, 1).getText().toLowerCase());
+ $(ButtonElement.class).get(1).click();
+ headerCell = grid.getHeaderCell(1, 1);
+ assertEquals("Header text not changed on column reorder.", "address",
+ headerCell.getText().toLowerCase());
+ assertEquals("Unexpected colspan", "1",
+ headerCell.getAttribute("colspan"));
+ headerCell = grid.getHeaderCell(1, 2);
+ assertEquals("Header text not changed on column reorder", "full name",
+ headerCell.getText().toLowerCase());
+ assertEquals("Unexpected colspan", "2",
+ headerCell.getAttribute("colspan"));
+
+ assertTrue("Error indicator not present",
+ isElementPresent(By.className("v-errorindicator")));
+
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidth.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidth.java
new file mode 100644
index 0000000000..98fa1ab6fd
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidth.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.renderer.HtmlRenderer;
+
+public class GridColumnAutoWidth extends AbstractTestUI {
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid grid = new Grid(createContainer());
+ grid.getColumn("fixed width narrow").setWidth(50);
+ grid.getColumn("fixed width wide").setWidth(200);
+
+ for (Object propertyId : grid.getContainerDataSource()
+ .getContainerPropertyIds()) {
+ Column column = grid.getColumn(propertyId);
+ column.setExpandRatio(0);
+ column.setRenderer(new HtmlRenderer());
+ grid.getHeaderRow(0).getCell(propertyId)
+ .setHtml("<span>" + column.getHeaderCaption() + "</span>");
+ }
+
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.setWidth("700px");
+ addComponent(grid);
+ }
+
+ private static Container.Indexed createContainer() {
+ IndexedContainer c = new IndexedContainer();
+ c.addContainerProperty("equal width", String.class,
+ "<span>equal width</span>");
+ c.addContainerProperty("short", String.class,
+ "<span>a very long cell content</span>");
+ c.addContainerProperty("a very long header content", String.class,
+ "<span>short</span>");
+ c.addContainerProperty("fixed width narrow", String.class,
+ "<span>fixed width narrow</span>");
+ c.addContainerProperty("fixed width wide", String.class,
+ "<span>fixed width wide</span>");
+ c.addItem();
+ return c;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClient.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClient.java
new file mode 100644
index 0000000000..0829e09de9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClient.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridColumnAutoWidthClientWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class GridColumnAutoWidthClient extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ addComponent(new TestWidgetComponent(
+ GridColumnAutoWidthClientWidget.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClientTest.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClientTest.java
new file mode 100644
index 0000000000..dcc14a967d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthClientTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.tests.annotations.TestCategory;
+
+@TestCategory("grid")
+public class GridColumnAutoWidthClientTest extends
+ AbstractGridColumnAutoWidthTest {
+ @Override
+ protected Class<?> getUIClass() {
+ return GridColumnAutoWidthClient.class;
+ }
+} \ No newline at end of file
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthServerTest.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthServerTest.java
new file mode 100644
index 0000000000..2f42b89eb1
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnAutoWidthServerTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.tests.annotations.TestCategory;
+
+@TestCategory("grid")
+public class GridColumnAutoWidthServerTest extends
+ AbstractGridColumnAutoWidthTest {
+ @Override
+ protected Class<?> getUIClass() {
+ return GridColumnAutoWidth.class;
+ }
+} \ No newline at end of file
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnExpand.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnExpand.java
new file mode 100644
index 0000000000..f8338f991a
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnExpand.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.util.PersonContainer;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CssLayout;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.themes.Reindeer;
+
+@Theme(Reindeer.THEME_NAME)
+public class GridColumnExpand extends AbstractTestUI {
+ private Grid grid;
+ private Label firstInfo = new Label();
+ private Label secondInfo = new Label();
+ private Column firstColumn;
+ private Column secondColumn;
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ grid = new Grid(PersonContainer.createWithTestData());
+ grid.removeAllColumns();
+ grid.addColumn("address.streetAddress");
+ grid.addColumn("lastName");
+ firstColumn = grid.getColumns().get(0);
+ secondColumn = grid.getColumns().get(1);
+
+ updateInfoLabels();
+ addComponent(grid);
+ addComponent(firstInfo);
+ addComponent(secondInfo);
+ addButtons();
+ }
+
+ private void addButtons() {
+ HorizontalLayout layout = new HorizontalLayout();
+ layout.addComponent(createButtons(firstColumn));
+ layout.addComponent(createButtons(secondColumn));
+ layout.setExpandRatio(layout.getComponent(1), 1);
+ addComponent(layout);
+ }
+
+ private Component createButtons(Column column) {
+ CssLayout layout = new CssLayout();
+ layout.addComponent(new Label("Column 1"));
+
+ CssLayout widthLayout = new CssLayout();
+ layout.addComponent(widthLayout);
+ widthLayout.addComponent(new Label("Width"));
+ widthLayout.addComponent(createWidthButton(column, -1));
+ widthLayout.addComponent(createWidthButton(column, 50));
+ widthLayout.addComponent(createWidthButton(column, 200));
+
+ CssLayout minLayout = new CssLayout();
+ layout.addComponent(minLayout);
+ minLayout.addComponent(new Label("Min width"));
+ minLayout.addComponent(createMinButton(column, -1));
+ minLayout.addComponent(createMinButton(column, 50));
+ minLayout.addComponent(createMinButton(column, 200));
+
+ CssLayout maxLayout = new CssLayout();
+ maxLayout.addComponent(new Label("Max width"));
+ maxLayout.addComponent(createMaxButton(column, -1));
+ maxLayout.addComponent(createMaxButton(column, 50));
+ maxLayout.addComponent(createMaxButton(column, 200));
+ layout.addComponent(maxLayout);
+
+ CssLayout expandLayout = new CssLayout();
+ expandLayout.addComponent(new Label("Expand ratio"));
+ expandLayout.addComponent(createExpandButton(column, -1));
+ expandLayout.addComponent(createExpandButton(column, 0));
+ expandLayout.addComponent(createExpandButton(column, 1));
+ expandLayout.addComponent(createExpandButton(column, 2));
+ layout.addComponent(expandLayout);
+
+ return layout;
+ }
+
+ private Component createWidthButton(final Column column, final double width) {
+ return new Button("" + width, new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ if (width >= 0) {
+ column.setWidth(width);
+ } else {
+ column.setWidthUndefined();
+ }
+ updateInfoLabels();
+ }
+ });
+ }
+
+ private Component createMinButton(final Column column, final double width) {
+ return new Button("" + width, new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ column.setMinimumWidth(width);
+ updateInfoLabels();
+ }
+ });
+ }
+
+ private Component createMaxButton(final Column column, final double width) {
+ return new Button("" + width, new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ column.setMaximumWidth(width);
+ updateInfoLabels();
+ }
+ });
+ }
+
+ private Component createExpandButton(final Column column, final int ratio) {
+ return new Button("" + ratio, new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ column.setExpandRatio(ratio);
+ updateInfoLabels();
+ }
+ });
+ }
+
+ private void updateInfoLabels() {
+ updateLabel(firstInfo, firstColumn);
+ updateLabel(secondInfo, secondColumn);
+ }
+
+ private void updateLabel(Label label, Column column) {
+ int expandRatio = column.getExpandRatio();
+ double minimumWidth = Math.round(column.getMinimumWidth() * 100) / 100;
+ double maximumWidth = Math.round(column.getMaximumWidth() * 100) / 100;
+ double width = Math.round(column.getWidth() * 100) / 100;
+ Object propertyId = column.getPropertyId();
+ label.setValue(String.format(
+ "[%s] Expand ratio: %s - min: %s - max: %s - width: %s",
+ propertyId, expandRatio, minimumWidth, maximumWidth, width));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridEditorUI.java b/uitest/src/com/vaadin/tests/components/grid/GridEditorUI.java
new file mode 100644
index 0000000000..fe4b4342a2
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridEditorUI.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.util.PersonContainer;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.PasswordField;
+import com.vaadin.ui.TextField;
+
+public class GridEditorUI extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ PersonContainer container = PersonContainer.createWithTestData();
+
+ Grid grid = new Grid(container);
+
+ // Don't use address since there's no converter
+ grid.removeColumn("address");
+
+ grid.setEditorEnabled(true);
+
+ grid.setEditorField("firstName", new PasswordField());
+
+ TextField lastNameField = (TextField) grid
+ .getEditorField("lastName");
+ lastNameField.setMaxLength(50);
+
+ grid.getEditorField("phoneNumber").setReadOnly(true);
+
+ addComponent(grid);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridEditorUITest.java b/uitest/src/com/vaadin/tests/components/grid/GridEditorUITest.java
new file mode 100644
index 0000000000..6c386eec03
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridEditorUITest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.testbench.elements.PasswordFieldElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridEditorUITest extends MultiBrowserTest {
+
+ @Test
+ public void testEditor() {
+ setDebug(true);
+ openTestURL();
+
+ assertFalse("Sanity check",
+ isElementPresent(PasswordFieldElement.class));
+
+ openEditor(5);
+ new Actions(getDriver()).sendKeys(Keys.ESCAPE).perform();
+
+ openEditor(10);
+
+ assertTrue("Edtor should be opened with a password field",
+ isElementPresent(PasswordFieldElement.class));
+
+ assertFalse("Notification was present",
+ isElementPresent(NotificationElement.class));
+ }
+
+ private void openEditor(int rowIndex) {
+ GridElement grid = $(GridElement.class).first();
+
+ GridCellElement cell = grid.getCell(rowIndex, 1);
+
+ new Actions(driver).moveToElement(cell).doubleClick().build().perform();
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridGeneratedProperties.java b/uitest/src/com/vaadin/tests/components/grid/GridGeneratedProperties.java
new file mode 100644
index 0000000000..2782a5fc6c
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridGeneratedProperties.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Container.Filterable;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Item;
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.GeneratedPropertyContainer;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.PropertyValueGenerator;
+import com.vaadin.data.util.filter.Compare;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Grid;
+
+public class GridGeneratedProperties extends AbstractTestUI {
+
+ private GeneratedPropertyContainer container;
+ static double MILES_CONVERSION = 0.6214d;
+ private Filter filter = new Compare.Greater("miles", 1d);
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ container = new GeneratedPropertyContainer(createContainer());
+ Grid grid = new Grid(container);
+ addComponent(grid);
+
+ container.addGeneratedProperty("miles",
+ new PropertyValueGenerator<Double>() {
+
+ @Override
+ public Double getValue(Item item, Object itemId,
+ Object propertyId) {
+ return (Double) item.getItemProperty("km").getValue()
+ * MILES_CONVERSION;
+ }
+
+ @Override
+ public Class<Double> getType() {
+ return Double.class;
+ }
+
+ @Override
+ public Filter modifyFilter(Filter filter)
+ throws UnsupportedFilterException {
+ if (filter instanceof Compare.Greater) {
+ Double value = (Double) ((Compare.Greater) filter)
+ .getValue();
+ value = value / MILES_CONVERSION;
+ return new Compare.Greater("km", value);
+ }
+ return super.modifyFilter(filter);
+ }
+ });
+
+ final Button filterButton = new Button("Add filter");
+ filterButton.addClickListener(new ClickListener() {
+
+ boolean active = false;
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ if (active) {
+ ((Filterable) container).removeContainerFilter(filter);
+ filterButton.setCaption("Add filter");
+ active = false;
+ return;
+ }
+ ((Filterable) container).addContainerFilter(filter);
+ filterButton.setCaption("Remove filter");
+ active = true;
+ }
+ });
+
+ container.addGeneratedProperty("foo",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return item.getItemProperty("foo").getValue() + " "
+ + item.getItemProperty("bar").getValue();
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+ container.removeContainerProperty("bar");
+ container.addGeneratedProperty("baz",
+ new PropertyValueGenerator<Integer>() {
+
+ @Override
+ public Integer getValue(Item item, Object itemId,
+ Object propertyId) {
+ return (Integer) item.getItemProperty("bar").getValue();
+ }
+
+ @Override
+ public Class<Integer> getType() {
+ return Integer.class;
+ }
+
+ @Override
+ public SortOrder[] getSortProperties(SortOrder order) {
+ return Sort.by("bar", order.getDirection()).build()
+ .toArray(new SortOrder[1]);
+ }
+ });
+
+ addComponent(filterButton);
+ grid.sort(Sort.by("km").then("bar", SortDirection.DESCENDING));
+ }
+
+ private Indexed createContainer() {
+ Indexed container = new IndexedContainer();
+ container.addContainerProperty("foo", String.class, "foo");
+ container.addContainerProperty("bar", Integer.class, 0);
+ // km contains double values from 0.0 to 2.0
+ container.addContainerProperty("km", Double.class, 0);
+
+ for (int i = 0; i <= 100; ++i) {
+ Object itemId = container.addItem();
+ Item item = container.getItem(itemId);
+ item.getItemProperty("foo").setValue("foo");
+ item.getItemProperty("bar").setValue(i);
+ item.getItemProperty("km").setValue(i / 5.0d);
+ }
+
+ return container;
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "A Grid with GeneratedPropertyContainer";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 13334;
+ }
+
+} \ No newline at end of file
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridGeneratedPropertiesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridGeneratedPropertiesTest.java
new file mode 100644
index 0000000000..ffcd4c448f
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridGeneratedPropertiesTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridGeneratedPropertiesTest extends MultiBrowserTest {
+
+ @Test
+ public void testMilesColumnExists() {
+ openTestURL();
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("Miles header wasn't present.", "miles", grid
+ .getHeaderCell(0, 2).getText().toLowerCase());
+ }
+
+ @Test
+ public void testUnsortableGeneratedProperty() {
+ openTestURL();
+ GridElement grid = $(GridElement.class).first();
+
+ // Overwritten foo property should not be sortable
+ GridCellElement fooHeader = grid.getHeaderCell(0, 0);
+ fooHeader.click();
+ assertFalse("Column foo was unexpectedly sorted.", fooHeader
+ .getAttribute("class").contains("sort"));
+
+ // Generated property miles is not sortable
+ GridCellElement milesHeader = grid.getHeaderCell(0, 2);
+ milesHeader.click();
+ assertFalse("Column miles was unexpectedly sorted.", milesHeader
+ .getAttribute("class").contains("sort"));
+ }
+
+ @Test
+ public void testSortableGeneratedProperty() {
+ openTestURL();
+ GridElement grid = $(GridElement.class).first();
+
+ // Generated property baz is sortable
+ GridCellElement bazHeader = grid.getHeaderCell(0, 3);
+ bazHeader.click();
+ assertTrue("Column baz was not sorted ascending", bazHeader
+ .getAttribute("class").contains("sort-asc"));
+ bazHeader.click();
+ assertTrue("Column baz was not sorted descending", bazHeader
+ .getAttribute("class").contains("sort-desc"));
+ }
+
+ @Test
+ public void testInitialSorting() {
+ // Grid is sorted in this case by one visible and one nonexistent
+ // column. There should be no sort indicator.
+ setDebug(true);
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+
+ GridCellElement kmHeader = grid.getHeaderCell(0, 1);
+ assertFalse("Column km was unexpectedly sorted",
+ kmHeader.getAttribute("class").contains("sort-asc")
+ || kmHeader.getAttribute("class").contains("sort-desc"));
+ assertFalse("Unexpected client-side exception was visible",
+ isElementPresent(NotificationElement.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNames.java b/uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNames.java
new file mode 100644
index 0000000000..765cd01812
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNames.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.tests.components.beanitemcontainer.BeanItemContainerGenerator;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.FooterCell;
+import com.vaadin.ui.Grid.FooterRow;
+import com.vaadin.ui.Grid.HeaderCell;
+import com.vaadin.ui.Grid.HeaderRow;
+import com.vaadin.ui.Grid.SelectionMode;
+
+@Theme("valo")
+public class GridHeaderStyleNames extends AbstractTestUIWithLog {
+
+ private HeaderCell ageHeaderCell;
+ private HeaderCell mergedCityCountryCell;
+ private FooterCell ageFooterCell;
+ private HeaderRow headerRow;
+ private FooterRow footerRow;
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid grid = new Grid();
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.setContainerDataSource(BeanItemContainerGenerator
+ .createContainer(100));
+
+ ageHeaderCell = grid.getDefaultHeaderRow().getCell("age");
+
+ headerRow = grid.prependHeaderRow();
+ mergedCityCountryCell = headerRow.join("city", "country");
+ mergedCityCountryCell.setText("Merged cell");
+ addComponent(grid);
+
+ footerRow = grid.appendFooterRow();
+ ageFooterCell = footerRow.getCell("age");
+
+ getPage()
+ .getStyles()
+ .add(".age {background-image: linear-gradient(to bottom,green 2%, #efefef 98%) !important;}");
+ getPage()
+ .getStyles()
+ .add(".valo .v-grid-header .v-grid-cell.city-country {background-image: linear-gradient(to bottom,yellow 2%, #efefef 98%) !important;}");
+ getPage()
+ .getStyles()
+ .add(".valo .v-grid-footer .v-grid-cell.age-footer {background-image: linear-gradient(to bottom,blue 2%, #efefef 98%) !important;}");
+ getPage()
+ .getStyles()
+ .add(".valo .v-grid .v-grid-row.custom-row > * {background-image: linear-gradient(to bottom,purple 2%, #efefef 98%);}");
+
+ setCellStyles(true);
+ setRowStyles(true);
+
+ Button b = new Button("Toggle styles");
+ b.addClickListener(new ClickListener() {
+ private boolean stylesOn = true;
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ setCellStyles(!stylesOn);
+ setRowStyles(!stylesOn);
+ stylesOn = !stylesOn;
+ }
+ });
+ addComponent(b);
+ }
+
+ protected void setCellStyles(boolean set) {
+ if (set) {
+ ageHeaderCell.setStyleName("age");
+ ageFooterCell.setStyleName("age-footer");
+ mergedCityCountryCell.setStyleName("city-country");
+ } else {
+ ageHeaderCell.setStyleName(null);
+ ageFooterCell.setStyleName(null);
+ mergedCityCountryCell.setStyleName(null);
+ }
+
+ }
+
+ protected void setRowStyles(boolean set) {
+ if (set) {
+ headerRow.setStyleName("custom-row");
+ footerRow.setStyleName("custom-row");
+ } else {
+ headerRow.setStyleName(null);
+ footerRow.setStyleName(null);
+ }
+
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNamesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNamesTest.java
new file mode 100644
index 0000000000..0f70d66ad4
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridHeaderStyleNamesTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+
+@TestCategory("grid")
+public class GridHeaderStyleNamesTest extends SingleBrowserTest {
+
+ private GridElement grid;
+
+ @Before
+ public void findGridCells() {
+ openTestURL();
+ grid = $(GridElement.class).first();
+ }
+
+ private GridCellElement getMergedHeaderCell() {
+ return grid.getHeaderCell(0, 3);
+ }
+
+ private GridCellElement getAgeFooterCell() {
+ return grid.getFooterCell(0, 2);
+ }
+
+ @Test
+ public void cellStyleNamesCanBeAddedAndRemoved() {
+ ButtonElement toggleStyles = $(ButtonElement.class).caption(
+ "Toggle styles").first();
+
+ assertStylesSet(true);
+ toggleStyles.click();
+ assertStylesSet(false);
+ toggleStyles.click();
+ assertStylesSet(true);
+ }
+
+ @Test
+ public void rowStyleNamesCanBeAddedAndRemoved() {
+ ButtonElement toggleStyles = $(ButtonElement.class).caption(
+ "Toggle styles").first();
+
+ assertRowStylesSet(true);
+ toggleStyles.click();
+ assertRowStylesSet(false);
+ toggleStyles.click();
+ assertRowStylesSet(true);
+
+ }
+
+ private void assertStylesSet(boolean set) {
+ if (set) {
+ assertHasStyleName(
+ "Footer cell should have the assigned 'age-footer' class name",
+ getAgeFooterCell(), "age-footer");
+ assertHasStyleName(
+ "Header cell should have the assigned 'age' class name",
+ getAgeHeaderCell(), "age");
+ assertHasStyleName(
+ "The merged header cell should have the assigned 'city-country' class name",
+ getMergedHeaderCell(), "city-country");
+ } else {
+ assertHasNotStyleName(
+ "Footer cell should not have the removed 'age-footer' class name",
+ getAgeFooterCell(), "age-footer");
+ assertHasNotStyleName(
+ "Header cell should not have the removed 'age' class name",
+ getAgeHeaderCell(), "age");
+ assertHasNotStyleName(
+ "Ther merged header cell should not have the removed 'city-country' class name",
+ getMergedHeaderCell(), "city-country");
+ }
+ assertHasStyleName(
+ "The default v-grid-cell style name should not be removed from the header cell",
+ getAgeHeaderCell(), "v-grid-cell");
+ assertHasStyleName(
+ "The default v-grid-cell style name should not be removed from the footer cell",
+ getAgeFooterCell(), "v-grid-cell");
+ assertHasStyleName(
+ "The default v-grid-cell style name should not be removed from the merged header cell",
+ getMergedHeaderCell(), "v-grid-cell");
+
+ }
+
+ private void assertRowStylesSet(boolean set) {
+ if (set) {
+ assertHasStyleName(
+ "Footer row should have the assigned 'custom-row' class name",
+ getFooterRow(), "custom-row");
+ assertHasStyleName(
+ "Header row should have the assigned 'custom-row' class name",
+ getHeaderRow(), "custom-row");
+ } else {
+ assertHasNotStyleName(
+ "Footer row should not have the removed 'custom-row' class name",
+ getFooterRow(), "custom-row");
+ assertHasNotStyleName(
+ "Header row should not have the removed 'custom-row' class name",
+ getHeaderRow(), "custom-row");
+ }
+ assertHasStyleName(
+ "The default v-grid-row style name should not be removed from the header row",
+ getHeaderRow(), "v-grid-row");
+ assertHasStyleName(
+ "The default v-grid-row style name should not be removed from the footer row",
+ getFooterRow(), "v-grid-row");
+
+ }
+
+ private WebElement getAgeHeaderCell() {
+ return grid.getHeaderCell(1, 2);
+ }
+
+ private WebElement getFooterRow() {
+ return grid.getFooterRow(0);
+ }
+
+ private WebElement getHeaderRow() {
+ return grid.getHeaderRow(0);
+ }
+
+ private void assertHasStyleName(String message, WebElement element,
+ String stylename) {
+ if (!hasCssClass(element, stylename)) {
+ Assert.fail(message);
+ }
+ }
+
+ private void assertHasNotStyleName(String message, WebElement element,
+ String stylename) {
+ if (hasCssClass(element, stylename)) {
+ Assert.fail(message);
+ }
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridInTabSheet.java b/uitest/src/com/vaadin/tests/components/grid/GridInTabSheet.java
new file mode 100644
index 0000000000..6c7f254a0d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridInTabSheet.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TabSheet;
+
+public class GridInTabSheet extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ TabSheet sheet = new TabSheet();
+ final Grid grid = new Grid();
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.addColumn("count", Integer.class);
+ for (Integer i = 0; i < 3; ++i) {
+ grid.addRow(i);
+ }
+
+ sheet.addTab(grid);
+ sheet.addTab(new Label("Hidden"));
+
+ addComponent(sheet);
+ addComponent(new Button("Add row to Grid", new Button.ClickListener() {
+
+ private Integer k = 0;
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.addRow(100 + (k++));
+ }
+ }));
+ addComponent(new Button("Remove row from Grid",
+ new Button.ClickListener() {
+
+ private Integer k = 0;
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ Object firstItemId = grid.getContainerDataSource()
+ .firstItemId();
+ if (firstItemId != null) {
+ grid.getContainerDataSource().removeItem(
+ firstItemId);
+ }
+ }
+ }));
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridInTabSheetTest.java b/uitest/src/com/vaadin/tests/components/grid/GridInTabSheetTest.java
new file mode 100644
index 0000000000..cd165e4678
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridInTabSheetTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridInTabSheetTest extends MultiBrowserTest {
+
+ @Test
+ public void testRemoveAllRowsAndAddThreeNewOnes() {
+ setDebug(true);
+ openTestURL();
+
+ for (int i = 0; i < 3; ++i) {
+ removeGridRow();
+ }
+
+ for (int i = 0; i < 3; ++i) {
+ addGridRow();
+ assertEquals("" + (100 + i), getGridElement().getCell(i, 1)
+ .getText());
+ }
+ assertFalse("There was an unexpected error notification",
+ isElementPresent(NotificationElement.class));
+ }
+
+ private void removeGridRow() {
+ $(ButtonElement.class).caption("Remove row from Grid").first().click();
+ }
+
+ private void addGridRow() {
+ $(ButtonElement.class).caption("Add row to Grid").first().click();
+ }
+
+ private GridElement getGridElement() {
+ return $(GridElement.class).first();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInit.java b/uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInit.java
new file mode 100644
index 0000000000..10d4977623
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInit.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class GridMultiSelectionOnInit extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid grid = new Grid();
+ grid.addColumn("foo", String.class);
+ grid.addRow("Foo 1");
+ grid.addRow("Foo 2");
+ grid.setSelectionMode(SelectionMode.MULTI);
+ addComponent(grid);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInitTest.java b/uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInitTest.java
new file mode 100644
index 0000000000..5f5b4df8de
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridMultiSelectionOnInitTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class GridMultiSelectionOnInitTest extends MultiBrowserTest {
+
+ @Test
+ public void testSelectAllCheckBoxExists() {
+ openTestURL();
+ assertTrue("The select all checkbox was missing.",
+ $(GridElement.class).first().getHeaderCell(0, 0)
+ .isElementPresent(By.tagName("input")));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java
new file mode 100644
index 0000000000..ce64b3e9f3
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.VerticalLayout;
+
+@SuppressWarnings("serial")
+public class GridScrolling extends AbstractTestUI {
+
+ private Grid grid;
+
+ private IndexedContainer ds;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void setup(VaadinRequest request) {
+ // Build data source
+ ds = new IndexedContainer();
+
+ for (int col = 0; col < 5; col++) {
+ ds.addContainerProperty("col" + col, String.class, "");
+ }
+
+ for (int row = 0; row < 65536; row++) {
+ Item item = ds.addItem(Integer.valueOf(row));
+ for (int col = 0; col < 5; col++) {
+ item.getItemProperty("col" + col).setValue(
+ "(" + row + ", " + col + ")");
+ }
+ }
+
+ grid = new Grid(ds);
+
+ HorizontalLayout hl = new HorizontalLayout();
+ hl.addComponent(grid);
+ hl.setMargin(true);
+ hl.setSpacing(true);
+
+ VerticalLayout vl = new VerticalLayout();
+ vl.setSpacing(true);
+
+ // Add scroll buttons
+ Button scrollUpButton = new Button("Top", new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.scrollToStart();
+ }
+ });
+ scrollUpButton.setSizeFull();
+ vl.addComponent(scrollUpButton);
+
+ for (int i = 1; i < 7; ++i) {
+ final int row = (ds.size() / 7) * i;
+ Button scrollButton = new Button("Scroll to row " + row,
+ new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.scrollTo(Integer.valueOf(row),
+ ScrollDestination.MIDDLE);
+ }
+ });
+ scrollButton.setSizeFull();
+ vl.addComponent(scrollButton);
+ }
+
+ Button scrollDownButton = new Button("Bottom", new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.scrollToEnd();
+ }
+ });
+ scrollDownButton.setSizeFull();
+ vl.addComponent(scrollDownButton);
+
+ hl.addComponent(vl);
+ addComponent(hl);
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Test Grid programmatic scrolling features";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 13327;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java
new file mode 100644
index 0000000000..2ab0282102
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class GridSingleColumn extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+
+ IndexedContainer indexedContainer = new IndexedContainer();
+ indexedContainer.addContainerProperty("column1", String.class, "");
+
+ for (int i = 0; i < 100; i++) {
+ Item addItem = indexedContainer.addItem(i);
+ addItem.getItemProperty("column1").setValue("cell");
+ }
+
+ Grid grid = new Grid(indexedContainer);
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ Column column = grid.getColumn("column1");
+
+ column.setHeaderCaption("Header");
+
+ addComponent(grid);
+ grid.scrollTo(grid.getContainerDataSource().getIdByIndex(50));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Tests a single column grid";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return null;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java
new file mode 100644
index 0000000000..42eb2197bf
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridSingleColumnTest extends MultiBrowserTest {
+
+ @Test
+ public void testHeaderIsVisible() {
+ openTestURL();
+
+ GridCellElement cell = $(GridElement.class).first().getHeaderCell(0, 0);
+ Assert.assertTrue("No header available", cell.getText()
+ .equalsIgnoreCase("header"));
+ }
+
+ @Test
+ public void testScrollDidNotThrow() {
+ setDebug(true);
+ openTestURL();
+
+ Assert.assertFalse("Exception when scrolling on init",
+ isElementPresent(NotificationElement.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridThemeChange.java b/uitest/src/com/vaadin/tests/components/grid/GridThemeChange.java
new file mode 100644
index 0000000000..76f7e22ee0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridThemeChange.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class GridThemeChange extends AbstractTestUI {
+ private final List<String> themes = Arrays.asList("valo", "reindeer",
+ "runo", "chameleon", "base");
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ final Grid grid = new Grid();
+ grid.setSelectionMode(SelectionMode.SINGLE);
+
+ grid.addColumn("Theme");
+ for (String theme : themes) {
+ Object itemId = grid.addRow(theme);
+ if (theme.equals(getTheme())) {
+ grid.select(itemId);
+ }
+ }
+
+ grid.addSelectionListener(new SelectionListener() {
+ @Override
+ public void select(SelectionEvent event) {
+ Object selectedItemId = grid.getSelectedRow();
+ Object theme = grid.getContainerDataSource()
+ .getItem(selectedItemId).getItemProperty("Theme")
+ .getValue();
+ setTheme(String.valueOf(theme));
+ }
+ });
+
+ addComponent(grid);
+
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java b/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java
new file mode 100644
index 0000000000..5a21705b55
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridThemeChangeTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class GridThemeChangeTest extends MultiBrowserTest {
+ @Override
+ public List<DesiredCapabilities> getBrowsersToTest() {
+ // Seems like stylesheet onload is not fired on PhantomJS
+ // https://github.com/ariya/phantomjs/issues/12332
+ return super.getBrowsersExcludingPhantomJS();
+ }
+
+ @Test
+ public void testThemeChange() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+
+ int reindeerHeight = grid.getRow(0).getSize().getHeight();
+
+ grid.getCell(0, 0).click();
+
+ int valoHeight = grid.getRow(0).getSize().getHeight();
+
+ Assert.assertTrue(
+ "Row height should increase when changing from Reindeer to Valo",
+ valoHeight > reindeerHeight);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverter.java b/uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverter.java
new file mode 100644
index 0000000000..fba2bbf698
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverter.java
@@ -0,0 +1,37 @@
+package com.vaadin.tests.components.grid;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+
+public class GridUndefinedObjectConverter extends AbstractTestUI {
+ private static class Pojo {
+ private final String content;
+
+ public Pojo(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public String toString() {
+ return "Pojo:" + content;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("all")
+ protected void setup(VaadinRequest request) {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty("pojo", Pojo.class, new Pojo("foo"));
+ container.addContainerProperty("pojo object ", Object.class, new Pojo(
+ "bar"));
+ container.addContainerProperty("int", Integer.class, 1);
+ container.addContainerProperty("int object", Object.class, 2);
+ container.addContainerProperty("string", String.class, "foo");
+ container.addContainerProperty("string object", Object.class, "bar");
+ container.addItem();
+
+ addComponent(new Grid(container));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverterTest.java b/uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverterTest.java
new file mode 100644
index 0000000000..401bfda885
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridUndefinedObjectConverterTest.java
@@ -0,0 +1,25 @@
+package com.vaadin.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridUndefinedObjectConverterTest extends MultiBrowserTest {
+ @Test
+ public void testDefaultToStringRendering() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("pojo", "Pojo:foo", grid.getCell(0, 0).getText());
+ assertEquals("pojo object", "Pojo:bar", grid.getCell(0, 1).getText());
+ assertEquals("int", "1", grid.getCell(0, 2).getText());
+ assertEquals("int object", "2", grid.getCell(0, 3).getText());
+ assertEquals("string", "foo", grid.getCell(0, 4).getText());
+ assertEquals("string object", "bar", grid.getCell(0, 5).getText());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridWithoutRenderer.java b/uitest/src/com/vaadin/tests/components/grid/GridWithoutRenderer.java
new file mode 100644
index 0000000000..fd5e6fdc01
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridWithoutRenderer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.util.PersonContainer;
+import com.vaadin.ui.Grid;
+
+@Theme("valo")
+public class GridWithoutRenderer extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid grid = new Grid();
+ grid.setContainerDataSource(PersonContainer.createWithTestData());
+ addComponent(grid);
+
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridWithoutRendererTest.java b/uitest/src/com/vaadin/tests/components/grid/GridWithoutRendererTest.java
new file mode 100644
index 0000000000..5d6ffbd8a7
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridWithoutRendererTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+
+@TestCategory("grid")
+public class GridWithoutRendererTest extends SingleBrowserTest {
+
+ @Test
+ public void ensureNoError() {
+ openTestURL();
+ // WebElement errorIndicator = findElement(By
+ // .cssSelector("v-error-indicator"));
+ // System.out.println(errorIndicator);
+ List<WebElement> errorIndicator = findElements(By
+ .xpath("//span[@class='v-errorindicator']"));
+ Assert.assertTrue("There should not be an error indicator",
+ errorIndicator.isEmpty());
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumns.java b/uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumns.java
new file mode 100644
index 0000000000..b6da30d314
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumns.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class InitialFrozenColumns extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid grid = new Grid();
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ grid.addColumn("foo").setWidth(200);
+ grid.addColumn("bar").setWidth(200);
+ grid.addColumn("baz").setWidth(200);
+
+ grid.addRow("a", "b", "c");
+
+ grid.setFrozenColumnCount(2);
+
+ addComponent(grid);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumnsTest.java b/uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumnsTest.java
new file mode 100644
index 0000000000..7a6d37d089
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/InitialFrozenColumnsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class InitialFrozenColumnsTest extends MultiBrowserTest {
+ @Test
+ public void testInitialFrozenColumns() {
+ setDebug(true);
+ openTestURL();
+
+ Assert.assertFalse("Notification was present",
+ isElementPresent(NotificationElement.class));
+
+ WebElement cell = $(GridElement.class).first().getCell(0, 0);
+ assertTrue(cell.getAttribute("class").contains("frozen"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java b/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java
new file mode 100644
index 0000000000..ce15676b60
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.ui.Grid.AbstractRenderer;
+
+public class IntArrayRenderer extends AbstractRenderer<int[]> {
+ public IntArrayRenderer() {
+ super(int[].class);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderers.java b/uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderers.java
new file mode 100644
index 0000000000..4bfa244c22
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderers.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+
+public class JavaScriptRenderers extends AbstractTestUI {
+
+ public static class MyBean {
+ private int integer;
+ private String string;
+
+ public MyBean(int integer, String string) {
+ super();
+ this.integer = integer;
+ this.string = string;
+ }
+
+ public int getInteger() {
+ return integer;
+ }
+
+ public void setInteger(int integer) {
+ this.integer = integer;
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(String string) {
+ this.string = string;
+ }
+ }
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty("id", Integer.class, Integer.valueOf(0));
+ container.addContainerProperty("bean", MyBean.class, null);
+
+ for (int i = 0; i < 1000; i++) {
+ Integer itemId = Integer.valueOf(i);
+ Item item = container.addItem(itemId);
+ item.getItemProperty("id").setValue(itemId);
+ item.getItemProperty("bean").setValue(
+ new MyBean(i + 1, Integer.toString(i - 1)));
+ }
+
+ Grid grid = new Grid(container);
+
+ grid.getColumn("bean").setRenderer(new MyBeanJSRenderer());
+ grid.getColumn("bean").setWidth(250);
+
+ addComponent(grid);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderersTest.java b/uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderersTest.java
new file mode 100644
index 0000000000..96fd672ab1
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/JavaScriptRenderersTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class JavaScriptRenderersTest extends MultiBrowserTest {
+
+ @Test
+ public void testJavaScriptRenderer() {
+ setDebug(true);
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ GridCellElement cell_1_1 = grid.getCell(1, 1);
+
+ // Verify render functionality
+ Assert.assertEquals("Bean(2, 0)", cell_1_1.getText());
+
+ // Verify init functionality
+ Assert.assertEquals("1", cell_1_1.getAttribute("column"));
+
+ // Verify onbrowserevent
+ cell_1_1.click();
+ Assert.assertTrue(cell_1_1.getText().startsWith(
+ "Clicked 1 with key 1 at"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/MyBeanJSRenderer.java b/uitest/src/com/vaadin/tests/components/grid/MyBeanJSRenderer.java
new file mode 100644
index 0000000000..ccb94f5d2d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/MyBeanJSRenderer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.annotations.JavaScript;
+import com.vaadin.tests.components.grid.JavaScriptRenderers.MyBean;
+import com.vaadin.ui.renderer.AbstractJavaScriptRenderer;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@JavaScript("myBeanJsRenderer.js")
+public class MyBeanJSRenderer extends AbstractJavaScriptRenderer<MyBean> {
+
+ public MyBeanJSRenderer() {
+ super(MyBean.class);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java b/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java
new file mode 100644
index 0000000000..7b3390a7e7
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.tests.widgetset.client.grid.RowAwareRendererConnector.RowAwareRendererRpc;
+import com.vaadin.ui.Grid.AbstractRenderer;
+import com.vaadin.ui.Label;
+
+public class RowAwareRenderer extends AbstractRenderer<Void> {
+ public RowAwareRenderer(final Label debugLabel) {
+ super(Void.class);
+ registerRpc(new RowAwareRendererRpc() {
+ @Override
+ public void clicky(String key) {
+ Object itemId = getItemId(key);
+ debugLabel.setValue("key: " + key + ", itemId: " + itemId);
+ }
+ });
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/SelectDuringInit.java b/uitest/src/com/vaadin/tests/components/grid/SelectDuringInit.java
new file mode 100644
index 0000000000..d8394acd19
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/SelectDuringInit.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+
+public class SelectDuringInit extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid grid = new Grid();
+ grid.setSelectionMode(SelectionMode.MULTI);
+
+ grid.addColumn("value");
+ grid.addRow("row 1");
+ grid.addRow("row 2");
+ grid.addRow("row 3");
+
+ grid.select(Integer.valueOf(2));
+
+ addComponent(grid);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/SelectDuringInitTest.java b/uitest/src/com/vaadin/tests/components/grid/SelectDuringInitTest.java
new file mode 100644
index 0000000000..edfc8031a8
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/SelectDuringInitTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+
+public class SelectDuringInitTest extends SingleBrowserTest {
+
+ @Test
+ public void testSelectDuringInit() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+
+ Assert.assertTrue(grid.getRow(1).isSelected());
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/WidgetRenderers.java b/uitest/src/com/vaadin/tests/components/grid/WidgetRenderers.java
new file mode 100644
index 0000000000..0d51558cd1
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/WidgetRenderers.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.NativeButton;
+import com.vaadin.ui.renderer.ButtonRenderer;
+import com.vaadin.ui.renderer.ClickableRenderer.RendererClickEvent;
+import com.vaadin.ui.renderer.ClickableRenderer.RendererClickListener;
+import com.vaadin.ui.renderer.ImageRenderer;
+import com.vaadin.ui.renderer.ProgressBarRenderer;
+
+@SuppressWarnings("all")
+public class WidgetRenderers extends AbstractTestUI {
+
+ static final String PROPERTY_ID = "property id";
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ IndexedContainer container = new IndexedContainer();
+
+ container.addContainerProperty(ProgressBarRenderer.class, Double.class,
+ null);
+ container
+ .addContainerProperty(ButtonRenderer.class, String.class, null);
+ container.addContainerProperty(ImageRenderer.class, Resource.class,
+ null);
+ container.addContainerProperty(PROPERTY_ID, String.class, null);
+
+ final Item item = container.getItem(container.addItem());
+
+ item.getItemProperty(ProgressBarRenderer.class).setValue(0.3);
+ item.getItemProperty(ButtonRenderer.class).setValue("Click");
+ item.getItemProperty(ImageRenderer.class).setValue(
+ new ThemeResource("window/img/close.png"));
+ item.getItemProperty(PROPERTY_ID).setValue("Click");
+
+ final Grid grid = new Grid(container);
+
+ grid.setId("test-grid");
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ grid.getColumn(ProgressBarRenderer.class).setRenderer(
+ new ProgressBarRenderer());
+
+ grid.getColumn(ButtonRenderer.class).setRenderer(
+ new ButtonRenderer(new RendererClickListener() {
+ @Override
+ public void click(RendererClickEvent event) {
+ item.getItemProperty(ButtonRenderer.class).setValue(
+ "Clicked!");
+ }
+ }));
+
+ grid.getColumn(ImageRenderer.class).setRenderer(
+ new ImageRenderer(new RendererClickListener() {
+
+ @Override
+ public void click(RendererClickEvent event) {
+ item.getItemProperty(ImageRenderer.class).setValue(
+ new ThemeResource("window/img/maximize.png"));
+ }
+ }));
+
+ grid.getColumn(PROPERTY_ID).setRenderer(
+ new ButtonRenderer(new RendererClickListener() {
+ @Override
+ public void click(RendererClickEvent event) {
+ item.getItemProperty(PROPERTY_ID).setValue(
+ event.getPropertyId());
+ }
+ }));
+
+ addComponent(grid);
+
+ addComponent(new NativeButton("Change column order",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.setColumnOrder(ImageRenderer.class,
+ ProgressBarRenderer.class, ButtonRenderer.class);
+ }
+ }));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Tests the functionality of widget-based renderers";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return Integer.valueOf(13334);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/WidgetRenderersTest.java b/uitest/src/com/vaadin/tests/components/grid/WidgetRenderersTest.java
new file mode 100644
index 0000000000..5da92b2034
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/WidgetRenderersTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+/**
+ * TB tests for the various builtin widget-based renderers.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@TestCategory("grid")
+public class WidgetRenderersTest extends MultiBrowserTest {
+
+ @Test
+ public void testProgressBarRenderer() {
+ openTestURL();
+
+ assertTrue(getGridCell(0, 0).isElementPresent(
+ By.className("v-progressbar")));
+ }
+
+ @Test
+ public void testButtonRenderer() {
+ openTestURL();
+
+ WebElement button = getGridCell(0, 1).findElement(
+ By.className("v-nativebutton"));
+
+ button.click();
+
+ assertEquals("Clicked!", button.getText());
+ }
+
+ @Test
+ public void testButtonRendererAfterCellBeingFocused() {
+ openTestURL();
+
+ GridCellElement buttonCell = getGridCell(0, 1);
+ assertFalse("cell should not be focused before focusing",
+ buttonCell.isFocused());
+
+ // avoid clicking on the button
+ buttonCell.click(buttonCell.getSize().getWidth() - 10, 5);
+ assertTrue("cell should be focused after focusing",
+ buttonCell.isFocused());
+
+ WebElement button = buttonCell.findElement(By
+ .className("v-nativebutton"));
+ assertNotEquals("Button should not be clicked before click",
+ "Clicked!", button.getText());
+
+ new Actions(getDriver()).moveToElement(button).click().perform();
+ assertEquals("Button should be clicked after click", "Clicked!",
+ button.getText());
+ }
+
+ @Test
+ public void testImageRenderer() {
+ openTestURL();
+
+ WebElement image = getGridCell(0, 2).findElement(
+ By.className("gwt-Image"));
+
+ assertTrue(image.getAttribute("src").endsWith("window/img/close.png"));
+
+ image.click();
+
+ assertTrue(image.getAttribute("src")
+ .endsWith("window/img/maximize.png"));
+ }
+
+ @Test
+ public void testColumnReorder() {
+ setDebug(true);
+ openTestURL();
+
+ $(ButtonElement.class).caption("Change column order").first().click();
+
+ assertFalse("Notification was present",
+ isElementPresent(NotificationElement.class));
+
+ assertTrue(getGridCell(0, 0)
+ .isElementPresent(By.className("gwt-Image")));
+ assertTrue(getGridCell(0, 1).isElementPresent(
+ By.className("v-progressbar")));
+ assertTrue(getGridCell(0, 2).isElementPresent(
+ By.className("v-nativebutton")));
+ }
+
+ @Test
+ public void testPropertyIdInEvent() {
+ openTestURL();
+ WebElement button = getGridCell(0, 3).findElement(
+ By.className("v-nativebutton"));
+ button.click();
+ assertEquals(WidgetRenderers.PROPERTY_ID, button.getText());
+ }
+
+ GridCellElement getGridCell(int row, int col) {
+ return $(GridElement.class).first().getCell(row, col);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeatures.java
new file mode 100644
index 0000000000..8e1a80a830
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeatures.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Title;
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+import com.vaadin.ui.UI;
+
+@Widgetset(TestingWidgetSet.NAME)
+@Title("Escalator basic client features")
+public class EscalatorBasicClientFeatures extends UI {
+
+ @Override
+ public void init(VaadinRequest request) {
+ setContent(new TestWidgetComponent(
+ EscalatorBasicClientFeaturesWidget.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java
new file mode 100644
index 0000000000..92c7f3e6a6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("escalator")
+public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest {
+ protected static final String COLUMNS_AND_ROWS = "Columns and Rows";
+
+ protected static final String COLUMNS = "Columns";
+ protected static final String ADD_ONE_COLUMN_TO_BEGINNING = "Add one column to beginning";
+ protected static final String ADD_ONE_ROW_TO_BEGINNING = "Add one row to beginning";
+ protected static final String REMOVE_ONE_COLUMN_FROM_BEGINNING = "Remove one column from beginning";
+ protected static final String REMOVE_ONE_ROW_FROM_BEGINNING = "Remove one row from beginning";
+ protected static final String REMOVE_50_ROWS_FROM_BOTTOM = "Remove 50 rows from bottom";
+ protected static final String REMOVE_50_ROWS_FROM_ALMOST_BOTTOM = "Remove 50 rows from almost bottom";
+ protected static final String ADD_ONE_OF_EACH_ROW = "Add one of each row";
+ protected static final String RESIZE_FIRST_COLUMN_TO_MAX_WIDTH = "Resize first column to max width";
+ protected static final String RESIZE_FIRST_COLUMN_TO_100PX = "Resize first column to 100 px";
+
+ protected static final String HEADER_ROWS = "Header Rows";
+ protected static final String BODY_ROWS = "Body Rows";
+ protected static final String FOOTER_ROWS = "Footer Rows";
+
+ protected static final String REMOVE_ALL_INSERT_SCROLL = "Remove all, insert 30 and scroll 40px";
+
+ protected static final String GENERAL = "General";
+ protected static final String DETACH_ESCALATOR = "Detach Escalator";
+ protected static final String POPULATE_COLUMN_ROW = "Populate Escalator (columns, then rows)";
+ protected static final String POPULATE_ROW_COLUMN = "Populate Escalator (rows, then columns)";
+ protected static final String CLEAR_COLUMN_ROW = "Clear (columns, then rows)";
+ protected static final String CLEAR_ROW_COLUMN = "Clear (rows, then columns)";
+
+ protected static final String FEATURES = "Features";
+ protected static final String FROZEN_COLUMNS = "Frozen columns";
+ protected static final String FREEZE_1_COLUMN = "Freeze 1 column";
+ protected static final String FREEZE_0_COLUMNS = "Freeze 0 columns";
+ protected static final String COLUMN_SPANNING = "Column spanning";
+ protected static final String COLSPAN_NORMAL = "Apply normal colspan";
+ protected static final String COLSPAN_NONE = "Apply no colspan";
+
+ @Override
+ protected Class<?> getUIClass() {
+ return EscalatorBasicClientFeatures.class;
+ }
+
+ protected TestBenchElement getEscalator() {
+ By className = By.className("v-escalator");
+ if (isElementPresent(className)) {
+ return (TestBenchElement) findElement(className);
+ }
+ return null;
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ protected TestBenchElement getHeaderRow(int row) {
+ return getRow("thead", row);
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ protected TestBenchElement getBodyRow(int row) {
+ return getRow("tbody", row);
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ protected TestBenchElement getFooterRow(int row) {
+ return getRow("tfoot", row);
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ protected TestBenchElement getHeaderCell(int row, int col) {
+ return getCell("thead", row, col);
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ protected TestBenchElement getBodyCell(int row, int col) {
+ return getCell("tbody", row, col);
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ protected TestBenchElement getFooterCell(int row, int col) {
+ return getCell("tfoot", row, col);
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ private TestBenchElement getCell(String sectionTag, int row, int col) {
+ TestBenchElement rowElement = getRow(sectionTag, row);
+ By xpath = By.xpath("*[" + (col + 1) + "]");
+ if (rowElement != null && rowElement.isElementPresent(xpath)) {
+ return (TestBenchElement) rowElement.findElement(xpath);
+ }
+ return null;
+ }
+
+ /**
+ * @param row
+ * the index of the row element in the section. If negative, the
+ * calculation starts from the end (-1 is the last, -2 is the
+ * second-to-last etc)
+ */
+ private TestBenchElement getRow(String sectionTag, int row) {
+ TestBenchElement escalator = getEscalator();
+ WebElement tableSection = escalator.findElement(By.tagName(sectionTag));
+ By xpath;
+
+ if (row >= 0) {
+ int fromFirst = row + 1;
+ xpath = By.xpath("tr[" + fromFirst + "]");
+ } else {
+ int fromLast = Math.abs(row + 1);
+ xpath = By.xpath("tr[last() - " + fromLast + "]");
+ }
+ if (tableSection != null
+ && ((TestBenchElement) tableSection).isElementPresent(xpath)) {
+ return (TestBenchElement) tableSection.findElement(xpath);
+ }
+ return null;
+ }
+
+ protected void selectMenu(String menuCaption) {
+ TestBenchElement menuElement = getMenuElement(menuCaption);
+ Dimension size = menuElement.getSize();
+ new Actions(getDriver()).moveToElement(menuElement, size.width - 10,
+ size.height / 2).perform();
+ }
+
+ private TestBenchElement getMenuElement(String menuCaption) {
+ return (TestBenchElement) findElement(By.xpath("//td[text() = '"
+ + menuCaption + "']"));
+ }
+
+ protected void selectMenuPath(String... menuCaptions) {
+ new Actions(getDriver()).moveToElement(getMenuElement(menuCaptions[0]))
+ .click().perform();
+ for (int i = 1; i < menuCaptions.length - 1; ++i) {
+ selectMenu(menuCaptions[i]);
+ new Actions(getDriver()).moveByOffset(20, 0).perform();
+ }
+ new Actions(getDriver())
+ .moveToElement(
+ getMenuElement(menuCaptions[menuCaptions.length - 1]))
+ .click().perform();
+ }
+
+ protected void assertLogContains(String substring) {
+ assertTrue("log should've contained, but didn't: " + substring,
+ getLogText().contains(substring));
+ }
+
+ protected void assertLogDoesNotContain(String substring) {
+ assertFalse("log shouldn't have contained, but did: " + substring,
+ getLogText().contains(substring));
+ }
+
+ private String getLogText() {
+ WebElement log = findElement(By.cssSelector("#log"));
+ return log.getText();
+ }
+
+ protected void assertLogContainsInOrder(String... substrings) {
+ String log = getLogText();
+ int cursor = 0;
+ for (String substring : substrings) {
+ String remainingLog = log.substring(cursor, log.length());
+ int substringIndex = remainingLog.indexOf(substring);
+ if (substringIndex == -1) {
+ fail("substring \"" + substring
+ + "\" was not found in order from log.");
+ }
+
+ cursor += substringIndex + substring.length();
+ }
+ }
+
+ protected void scrollVerticallyTo(int px) {
+ executeScript("arguments[0].scrollTop = " + px, getVeticalScrollbar());
+ }
+
+ private TestBenchElement getVeticalScrollbar() {
+ return (TestBenchElement) getEscalator().findElement(
+ By.className("v-escalator-scroller-vertical"));
+ }
+
+ protected void scrollHorizontallyTo(int px) {
+ executeScript("arguments[0].scrollLeft = " + px,
+ getHorizontalScrollbar());
+ }
+
+ private TestBenchElement getHorizontalScrollbar() {
+ return (TestBenchElement) getEscalator().findElement(
+ By.className("v-escalator-scroller-horizontal"));
+ }
+
+ protected Object executeScript(String script, Object... args) {
+ return ((JavascriptExecutor) getDriver()).executeScript(script, args);
+ }
+
+ protected void populate() {
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorUpdaterUi.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorUpdaterUi.java
new file mode 100644
index 0000000000..ef997b3cae
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorUpdaterUi.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+import com.vaadin.ui.UI;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class EscalatorUpdaterUi extends UI {
+
+ @Override
+ protected void init(VaadinRequest request) {
+ setContent(new TestWidgetComponent(
+ EscalatorBasicClientFeaturesWidget.UpdaterLifetimeWidget.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java
new file mode 100644
index 0000000000..429f15bb47
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+import com.vaadin.ui.UI;
+
+/**
+ * Initializer shell for GridClientBasicFeatures test application
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Widgetset(TestingWidgetSet.NAME)
+public class GridBasicClientFeatures extends UI {
+
+ @Override
+ protected void init(VaadinRequest request) {
+ setContent(new TestWidgetComponent(GridBasicClientFeaturesWidget.class));
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java
new file mode 100644
index 0000000000..d0e076fd3b
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.GridElement;
+
+/**
+ * Variant of GridBasicFeaturesTest to be used with GridBasicClientFeatures.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class GridBasicClientFeaturesTest extends GridBasicFeaturesTest {
+
+ private boolean composite = false;
+
+ @Override
+ protected Class<?> getUIClass() {
+ return GridBasicClientFeatures.class;
+ }
+
+ @Override
+ protected String getDeploymentPath() {
+ String path = super.getDeploymentPath();
+ if (composite) {
+ path += (path.contains("?") ? "&" : "?") + "composite";
+ }
+ return path;
+ }
+
+ protected void setUseComposite(boolean useComposite) {
+ composite = useComposite;
+ }
+
+ @Override
+ protected void selectMenu(String menuCaption) {
+ WebElement menuElement = getMenuElement(menuCaption);
+ Dimension size = menuElement.getSize();
+ new Actions(getDriver()).moveToElement(menuElement, size.width - 10,
+ size.height / 2).perform();
+ }
+
+ private WebElement getMenuElement(String menuCaption) {
+ return getDriver().findElement(
+ By.xpath("//td[text() = '" + menuCaption + "']"));
+ }
+
+ @Override
+ protected void selectMenuPath(String... menuCaptions) {
+ new Actions(getDriver()).moveToElement(getMenuElement(menuCaptions[0]))
+ .click().perform();
+ for (int i = 1; i < menuCaptions.length - 1; ++i) {
+ selectMenu(menuCaptions[i]);
+ new Actions(getDriver()).moveByOffset(20, 0).perform();
+ }
+ new Actions(getDriver())
+ .moveToElement(
+ getMenuElement(menuCaptions[menuCaptions.length - 1]))
+ .click().perform();
+ }
+
+ @Override
+ protected GridElement getGridElement() {
+ if (composite) {
+ // Composite requires the basic client features widget for subparts
+ return ((TestBenchElement) findElement(By
+ .vaadin("//TestWidgetComponent")))
+ .wrap(GridElement.class);
+ } else {
+ return super.getGridElement();
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
new file mode 100644
index 0000000000..7a625e2f25
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
@@ -0,0 +1,1026 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.tests.components.AbstractComponentTest;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.CellReference;
+import com.vaadin.ui.Grid.CellStyleGenerator;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.FooterCell;
+import com.vaadin.ui.Grid.HeaderCell;
+import com.vaadin.ui.Grid.HeaderRow;
+import com.vaadin.ui.Grid.MultiSelectionModel;
+import com.vaadin.ui.Grid.RowReference;
+import com.vaadin.ui.Grid.RowStyleGenerator;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.renderer.DateRenderer;
+import com.vaadin.ui.renderer.HtmlRenderer;
+import com.vaadin.ui.renderer.NumberRenderer;
+
+/**
+ * Tests the basic features like columns, footers and headers
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridBasicFeatures extends AbstractComponentTest<Grid> {
+
+ public static final String ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4 = "Row numbers for 3/4";
+ public static final String ROW_STYLE_GENERATOR_NONE = "None";
+ public static final String ROW_STYLE_GENERATOR_ROW_NUMBERS = "Row numbers";
+ public static final String CELL_STYLE_GENERATOR_NONE = "None";
+ public static final String CELL_STYLE_GENERATOR_PROPERTY_TO_STRING = "Property to string";
+ public static final String CELL_STYLE_GENERATOR_SPECIAL = "Special for 1/4 Column 1";
+ private static final int MANUALLY_FORMATTED_COLUMNS = 5;
+ public static final int COLUMNS = 12;
+ public static final int ROWS = 1000;
+
+ private int containerDelay = 0;
+
+ private IndexedContainer ds;
+ private Grid grid;
+ private SelectionListener selectionListener = new SelectionListener() {
+
+ @Override
+ public void select(SelectionEvent event) {
+ Iterator<Object> iter = event.getAdded().iterator();
+ Object addedRow = (iter.hasNext() ? iter.next() : "none");
+ iter = event.getRemoved().iterator();
+ Object removedRow = (iter.hasNext() ? iter.next() : "none");
+ log("SelectionEvent: Added " + addedRow + ", Removed " + removedRow);
+ }
+ };
+
+ private ItemClickListener itemClickListener = new ItemClickListener() {
+
+ @Override
+ public void itemClick(ItemClickEvent event) {
+ log("Item " + (event.isDoubleClick() ? "double " : "")
+ + "click on " + event.getPropertyId() + ", item "
+ + event.getItemId());
+ }
+ };
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Grid constructComponent() {
+
+ // Build data source
+ ds = new IndexedContainer() {
+ @Override
+ public List<Object> getItemIds(int startIndex, int numberOfIds) {
+ log("Requested items " + startIndex + " - "
+ + (startIndex + numberOfIds));
+ if (containerDelay > 0) {
+ try {
+ Thread.sleep(containerDelay);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ return super.getItemIds(startIndex, numberOfIds);
+ }
+ };
+
+ {
+ int col = 0;
+ for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; col++) {
+ ds.addContainerProperty(getColumnProperty(col), String.class,
+ "");
+ }
+
+ ds.addContainerProperty(getColumnProperty(col++), Integer.class,
+ Integer.valueOf(0));
+ ds.addContainerProperty(getColumnProperty(col++), Date.class,
+ new Date());
+ ds.addContainerProperty(getColumnProperty(col++), String.class, "");
+
+ // Random numbers
+ ds.addContainerProperty(getColumnProperty(col++), Integer.class, 0);
+ ds.addContainerProperty(getColumnProperty(col++), Integer.class, 0);
+
+ }
+
+ {
+ Random rand = new Random();
+ rand.setSeed(13334);
+ long timestamp = 0;
+ for (int row = 0; row < ROWS; row++) {
+ Item item = ds.addItem(Integer.valueOf(row));
+ int col = 0;
+ for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; col++) {
+ item.getItemProperty(getColumnProperty(col)).setValue(
+ "(" + row + ", " + col + ")");
+ }
+ item.getItemProperty(getColumnProperty(1)).setReadOnly(true);
+
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ Integer.valueOf(row));
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ new Date(timestamp));
+ timestamp += 91250000; // a bit over a day, just to get
+ // variation
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ "<b>" + row + "</b>");
+
+ // Random numbers
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ rand.nextInt());
+ // Random between 0 - 5 to test multisorting
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ rand.nextInt(5));
+ }
+ }
+
+ // Create grid
+ Grid grid = new Grid(ds);
+
+ {
+ int col = grid.getContainerDataSource().getContainerPropertyIds()
+ .size()
+ - MANUALLY_FORMATTED_COLUMNS;
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new NumberRenderer(new DecimalFormat("0,000.00",
+ DecimalFormatSymbols.getInstance(new Locale("fi",
+ "FI")))));
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new DateRenderer(new SimpleDateFormat("dd.MM.yy HH:mm")));
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new HtmlRenderer());
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new NumberRenderer());
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new NumberRenderer());
+ }
+
+ // Create footer
+ grid.appendFooterRow();
+ grid.setFooterVisible(false);
+
+ // Add footer values (header values are automatically created)
+ for (int col = 0; col < COLUMNS; col++) {
+ grid.getFooterRow(0).getCell(getColumnProperty(col))
+ .setText("Footer " + col);
+ }
+
+ // Set varying column widths
+ for (int col = 0; col < COLUMNS; col++) {
+ grid.getColumn(getColumnProperty(col)).setWidth(100 + col * 50);
+ }
+
+ grid.addSortListener(new SortListener() {
+ @Override
+ public void sort(SortEvent event) {
+
+ log("SortOrderChangeEvent: isUserOriginated? "
+ + event.isUserOriginated());
+ }
+ });
+
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ grid.getEditorField(getColumnProperty(3)).setReadOnly(true);
+
+ createGridActions();
+
+ createColumnActions();
+
+ createPropertyActions();
+
+ createHeaderActions();
+
+ createFooterActions();
+
+ createRowActions();
+
+ createEditorActions();
+
+ addHeightActions();
+
+ addFilterActions();
+
+ this.grid = grid;
+ return grid;
+ }
+
+ private void addFilterActions() {
+ createClickAction("Column 1 starts with \"(23\"", "Filter",
+ new Command<Grid, Void>() {
+ @Override
+ public void execute(Grid grid, Void value, Object data) {
+ ds.addContainerFilter(new Filter() {
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ return item.getItemProperty("Column 1")
+ .getValue().toString()
+ .startsWith("(23");
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return propertyId.equals("Column 1");
+ }
+ });
+ }
+ }, null);
+
+ createClickAction("Add impassable filter", "Filter",
+ new Command<Grid, Void>() {
+ @Override
+ public void execute(Grid c, Void value, Object data) {
+ ds.addContainerFilter(new Filter() {
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ return false;
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return true;
+ }
+ });
+ }
+ }, null);
+ }
+
+ protected void createGridActions() {
+ LinkedHashMap<String, String> primaryStyleNames = new LinkedHashMap<String, String>();
+ primaryStyleNames.put("v-grid", "v-grid");
+ primaryStyleNames.put("v-escalator", "v-escalator");
+ primaryStyleNames.put("my-grid", "my-grid");
+
+ createMultiClickAction("Primary style name", "State",
+ primaryStyleNames, new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ grid.setPrimaryStyleName(value);
+
+ }
+ }, primaryStyleNames.get("v-grid"));
+
+ LinkedHashMap<String, SelectionMode> selectionModes = new LinkedHashMap<String, Grid.SelectionMode>();
+ selectionModes.put("single", SelectionMode.SINGLE);
+ selectionModes.put("multi", SelectionMode.MULTI);
+ selectionModes.put("none", SelectionMode.NONE);
+ createSelectAction("Selection mode", "State", selectionModes, "none",
+ new Command<Grid, Grid.SelectionMode>() {
+ @Override
+ public void execute(Grid grid, SelectionMode selectionMode,
+ Object data) {
+ grid.setSelectionMode(selectionMode);
+ if (selectionMode == SelectionMode.SINGLE) {
+ grid.addSelectionListener(selectionListener);
+ } else {
+ grid.removeSelectionListener(selectionListener);
+ }
+ }
+ });
+
+ LinkedHashMap<String, Integer> selectionLimits = new LinkedHashMap<String, Integer>();
+ selectionLimits.put("2", Integer.valueOf(2));
+ selectionLimits.put("1000", Integer.valueOf(1000));
+ selectionLimits.put("Integer.MAX_VALUE",
+ Integer.valueOf(Integer.MAX_VALUE));
+ createSelectAction("Selection limit", "State", selectionLimits, "1000",
+ new Command<Grid, Integer>() {
+ @Override
+ public void execute(Grid grid, Integer limit, Object data) {
+ if (!(grid.getSelectionModel() instanceof MultiSelectionModel)) {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ }
+
+ ((MultiSelectionModel) grid.getSelectionModel())
+ .setSelectionLimit(limit.intValue());
+ }
+ });
+
+ LinkedHashMap<String, List<SortOrder>> sortableProperties = new LinkedHashMap<String, List<SortOrder>>();
+ for (Object propertyId : ds.getSortableContainerPropertyIds()) {
+ sortableProperties.put(propertyId + ", ASC", Sort.by(propertyId)
+ .build());
+ sortableProperties.put(propertyId + ", DESC",
+ Sort.by(propertyId, SortDirection.DESCENDING).build());
+ }
+ createSelectAction("Sort by column", "State", sortableProperties,
+ "Column 9, ascending", new Command<Grid, List<SortOrder>>() {
+ @Override
+ public void execute(Grid grid, List<SortOrder> sortOrder,
+ Object data) {
+ grid.setSortOrder(sortOrder);
+ }
+ });
+
+ createBooleanAction("Reverse Grid Columns", "State", false,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid c, Boolean value, Object data) {
+ List<Object> ids = new ArrayList<Object>();
+ ids.addAll(ds.getContainerPropertyIds());
+ if (!value) {
+ c.setColumnOrder(ids.toArray());
+ } else {
+ Object[] idsArray = new Object[ids.size()];
+ for (int i = 0; i < ids.size(); ++i) {
+ idsArray[i] = ids.get((ids.size() - 1) - i);
+ }
+ c.setColumnOrder(idsArray);
+ }
+ }
+ });
+
+ LinkedHashMap<String, CellStyleGenerator> cellStyleGenerators = new LinkedHashMap<String, CellStyleGenerator>();
+ LinkedHashMap<String, RowStyleGenerator> rowStyleGenerators = new LinkedHashMap<String, RowStyleGenerator>();
+ rowStyleGenerators.put(ROW_STYLE_GENERATOR_NONE, null);
+ rowStyleGenerators.put(ROW_STYLE_GENERATOR_ROW_NUMBERS,
+ new RowStyleGenerator() {
+ @Override
+ public String getStyle(RowReference rowReference) {
+ return "row" + rowReference.getItemId();
+ }
+ });
+ rowStyleGenerators.put(ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4,
+ new RowStyleGenerator() {
+ @Override
+ public String getStyle(RowReference rowReference) {
+ int rowIndex = ((Integer) rowReference.getItemId())
+ .intValue();
+
+ if (rowIndex % 4 == 0) {
+ return null;
+ } else {
+ return "row" + rowReference.getItemId();
+ }
+ }
+ });
+ cellStyleGenerators.put(CELL_STYLE_GENERATOR_NONE, null);
+ cellStyleGenerators.put(CELL_STYLE_GENERATOR_PROPERTY_TO_STRING,
+ new CellStyleGenerator() {
+ @Override
+ public String getStyle(CellReference cellReference) {
+ return cellReference.getPropertyId().toString()
+ .replace(' ', '-');
+ }
+ });
+ cellStyleGenerators.put(CELL_STYLE_GENERATOR_SPECIAL,
+ new CellStyleGenerator() {
+ @Override
+ public String getStyle(CellReference cellReference) {
+ int rowIndex = ((Integer) cellReference.getItemId())
+ .intValue();
+ Object propertyId = cellReference.getPropertyId();
+ if (rowIndex % 4 == 1) {
+ return null;
+ } else if (rowIndex % 4 == 3
+ && "Column 1".equals(propertyId)) {
+ return null;
+ }
+ return propertyId.toString().replace(' ', '_');
+ }
+ });
+
+ createSelectAction("Row style generator", "State", rowStyleGenerators,
+ CELL_STYLE_GENERATOR_NONE,
+ new Command<Grid, RowStyleGenerator>() {
+ @Override
+ public void execute(Grid grid, RowStyleGenerator generator,
+ Object data) {
+ grid.setRowStyleGenerator(generator);
+ }
+ });
+
+ createSelectAction("Cell style generator", "State",
+ cellStyleGenerators, CELL_STYLE_GENERATOR_NONE,
+ new Command<Grid, CellStyleGenerator>() {
+ @Override
+ public void execute(Grid grid,
+ CellStyleGenerator generator, Object data) {
+ grid.setCellStyleGenerator(generator);
+ }
+ });
+
+ LinkedHashMap<String, Integer> frozenOptions = new LinkedHashMap<String, Integer>();
+ for (int i = -1; i <= COLUMNS; i++) {
+ frozenOptions.put(String.valueOf(i), Integer.valueOf(i));
+ }
+ createSelectAction("Frozen column count", "State", frozenOptions, "0",
+ new Command<Grid, Integer>() {
+ @Override
+ public void execute(Grid c, Integer value, Object data) {
+ c.setFrozenColumnCount(value.intValue());
+ }
+ });
+
+ LinkedHashMap<String, Integer> containerDelayValues = new LinkedHashMap<String, Integer>();
+ for (int delay : new int[] { 0, 500, 2000, 10000 }) {
+ containerDelayValues.put(String.valueOf(delay),
+ Integer.valueOf(delay));
+ }
+
+ createSelectAction("Container delay", "State", containerDelayValues,
+ "0", new Command<Grid, Integer>() {
+ @Override
+ public void execute(Grid grid, Integer delay, Object data) {
+ containerDelay = delay.intValue();
+ }
+ });
+
+ createBooleanAction("ItemClickListener", "State", false,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid c, Boolean value, Object data) {
+ if (!value) {
+ c.removeItemClickListener(itemClickListener);
+ } else {
+ c.addItemClickListener(itemClickListener);
+ }
+ }
+
+ });
+ }
+
+ protected void createHeaderActions() {
+ createCategory("Header", null);
+
+ createBooleanAction("Visible", "Header", true,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value, Object data) {
+ grid.setHeaderVisible(value);
+ }
+ });
+
+ LinkedHashMap<String, String> defaultRows = new LinkedHashMap<String, String>();
+ defaultRows.put("Top", "Top");
+ defaultRows.put("Bottom", "Bottom");
+ defaultRows.put("Unset", "Unset");
+
+ createMultiClickAction("Default row", "Header", defaultRows,
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ HeaderRow defaultRow = null;
+ if (value.equals("Top")) {
+ defaultRow = grid.getHeaderRow(0);
+ } else if (value.equals("Bottom")) {
+ defaultRow = grid.getHeaderRow(grid
+ .getHeaderRowCount() - 1);
+ }
+ grid.setDefaultHeaderRow(defaultRow);
+ }
+
+ }, defaultRows.get("Top"));
+
+ createClickAction("Prepend row", "Header", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.prependHeaderRow();
+ }
+
+ }, null);
+ createClickAction("Append row", "Header", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.appendHeaderRow();
+ }
+
+ }, null);
+
+ createClickAction("Remove top row", "Header",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.removeHeaderRow(0);
+ }
+
+ }, null);
+ createClickAction("Remove bottom row", "Header",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.removeHeaderRow(grid.getHeaderRowCount() - 1);
+ }
+
+ }, null);
+ }
+
+ protected void createFooterActions() {
+ createCategory("Footer", null);
+
+ createBooleanAction("Visible", "Footer", false,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value, Object data) {
+ grid.setFooterVisible(value);
+ }
+ });
+
+ createClickAction("Prepend row", "Footer", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.prependFooterRow();
+ }
+
+ }, null);
+ createClickAction("Append row", "Footer", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.appendFooterRow();
+ }
+
+ }, null);
+
+ createClickAction("Remove top row", "Footer",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.removeFooterRow(0);
+ }
+
+ }, null);
+ createClickAction("Remove bottom row", "Footer",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.removeFooterRow(grid.getFooterRowCount() - 1);
+ }
+
+ }, null);
+ }
+
+ protected void createColumnActions() {
+ createCategory("Columns", null);
+
+ for (int c = 0; c < COLUMNS; c++) {
+ final int index = c;
+ createCategory(getColumnProperty(c), "Columns");
+
+ createClickAction("Add / Remove", getColumnProperty(c),
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ String columnProperty = getColumnProperty((Integer) data);
+ if (grid.getColumn(columnProperty) == null) {
+ grid.addColumn(columnProperty);
+ } else {
+ grid.removeColumn(columnProperty);
+ }
+ }
+ }, null, c);
+
+ createBooleanAction("Sortable", getColumnProperty(c), true,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value,
+ Object columnIndex) {
+ Object propertyId = getColumnProperty((Integer) columnIndex);
+ Column column = grid.getColumn(propertyId);
+ column.setSortable(value);
+ }
+ }, c);
+
+ createCategory("Column " + c + " Width", getColumnProperty(c));
+
+ createClickAction("Auto", "Column " + c + " Width",
+ new Command<Grid, Integer>() {
+
+ @Override
+ public void execute(Grid grid, Integer value,
+ Object columnIndex) {
+ Object propertyId = getColumnProperty((Integer) columnIndex);
+ Column column = grid.getColumn(propertyId);
+ column.setWidthUndefined();
+ }
+ }, -1, c);
+
+ createClickAction("25.5px", "Column " + c + " Width",
+ new Command<Grid, Void>() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void execute(Grid grid, Void value,
+ Object columnIndex) {
+ grid.getColumns().get((Integer) columnIndex)
+ .setWidth(25.5);
+ }
+ }, null, c);
+
+ for (int w = 50; w < 300; w += 50) {
+ createClickAction(w + "px", "Column " + c + " Width",
+ new Command<Grid, Integer>() {
+
+ @Override
+ public void execute(Grid grid, Integer value,
+ Object columnIndex) {
+ Object propertyId = getColumnProperty((Integer) columnIndex);
+ Column column = grid.getColumn(propertyId);
+ column.setWidth(value);
+ }
+ }, w, c);
+ }
+
+ LinkedHashMap<String, GridStaticCellType> defaultRows = new LinkedHashMap<String, GridStaticCellType>();
+ defaultRows.put("Text Header", GridStaticCellType.TEXT);
+ defaultRows.put("Html Header ", GridStaticCellType.HTML);
+ defaultRows.put("Widget Header", GridStaticCellType.WIDGET);
+
+ createMultiClickAction("Header Type", getColumnProperty(c),
+ defaultRows, new Command<Grid, GridStaticCellType>() {
+
+ @Override
+ public void execute(Grid grid,
+ GridStaticCellType value, Object columnIndex) {
+ final Object propertyId = getColumnProperty((Integer) columnIndex);
+ final HeaderCell cell = grid.getDefaultHeaderRow()
+ .getCell(propertyId);
+ switch (value) {
+ case TEXT:
+ cell.setText("Text Header");
+ break;
+ case HTML:
+ cell.setHtml("HTML Header");
+ break;
+ case WIDGET:
+ cell.setComponent(new Button("Button Header",
+ new ClickListener() {
+
+ @Override
+ public void buttonClick(
+ ClickEvent event) {
+ log("Button clicked!");
+ }
+ }));
+ default:
+ break;
+ }
+ }
+
+ }, c);
+
+ defaultRows = new LinkedHashMap<String, GridStaticCellType>();
+ defaultRows.put("Text Footer", GridStaticCellType.TEXT);
+ defaultRows.put("Html Footer", GridStaticCellType.HTML);
+ defaultRows.put("Widget Footer", GridStaticCellType.WIDGET);
+
+ createMultiClickAction("Footer Type", getColumnProperty(c),
+ defaultRows, new Command<Grid, GridStaticCellType>() {
+
+ @Override
+ public void execute(Grid grid,
+ GridStaticCellType value, Object columnIndex) {
+ final Object propertyId = getColumnProperty((Integer) columnIndex);
+ final FooterCell cell = grid.getFooterRow(0)
+ .getCell(propertyId);
+ switch (value) {
+ case TEXT:
+ cell.setText("Text Footer");
+ break;
+ case HTML:
+ cell.setHtml("HTML Footer");
+ break;
+ case WIDGET:
+ cell.setComponent(new Button("Button Footer",
+ new ClickListener() {
+
+ @Override
+ public void buttonClick(
+ ClickEvent event) {
+ log("Button clicked!");
+ }
+ }));
+ default:
+ break;
+ }
+ }
+
+ }, c);
+ }
+ }
+
+ private static String getColumnProperty(int c) {
+ return "Column " + c;
+ }
+
+ protected void createPropertyActions() {
+ createCategory("Properties", null);
+
+ createBooleanAction("Prepend property", "Properties", false,
+ new Command<Grid, Boolean>() {
+ private final Object propertyId = new Object();
+
+ @Override
+ public void execute(Grid c, Boolean enable, Object data) {
+ if (enable.booleanValue()) {
+ ds.addContainerProperty(propertyId, String.class,
+ "property value");
+ grid.getColumn(propertyId).setHeaderCaption(
+ "new property");
+ grid.setColumnOrder(propertyId);
+ } else {
+ ds.removeContainerProperty(propertyId);
+ }
+ }
+ }, null);
+ }
+
+ protected void createRowActions() {
+ createCategory("Body rows", null);
+
+ class NewRowCommand implements Command<Grid, String> {
+ private final int index;
+
+ public NewRowCommand() {
+ this(0);
+ }
+
+ public NewRowCommand(int index) {
+ this.index = index;
+ }
+
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ Item item = ds.addItemAt(index, new Object());
+ for (int i = 0; i < COLUMNS; i++) {
+ Class<?> type = ds.getType(getColumnProperty(i));
+ if (String.class.isAssignableFrom(type)) {
+ Property<String> itemProperty = getProperty(item, i);
+ itemProperty.setValue("newcell: " + i);
+ } else if (Integer.class.isAssignableFrom(type)) {
+ Property<Integer> itemProperty = getProperty(item, i);
+ itemProperty.setValue(Integer.valueOf(i));
+ } else {
+ // let the default value be taken implicitly.
+ }
+ }
+ }
+
+ private <T extends Object> Property<T> getProperty(Item item, int i) {
+ @SuppressWarnings("unchecked")
+ Property<T> itemProperty = item
+ .getItemProperty(getColumnProperty(i));
+ return itemProperty;
+ }
+ }
+ final NewRowCommand newRowCommand = new NewRowCommand();
+
+ createClickAction("Add 18 rows", "Body rows",
+ new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ for (int i = 0; i < 18; i++) {
+ newRowCommand.execute(c, value, data);
+ }
+ }
+ }, null);
+
+ createClickAction("Add first row", "Body rows", newRowCommand, null);
+
+ createClickAction("Add third row", "Body rows", new NewRowCommand(2),
+ null);
+
+ createClickAction("Remove first row", "Body rows",
+ new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ Object firstItemId = ds.getIdByIndex(0);
+ ds.removeItem(firstItemId);
+ }
+ }, null);
+
+ createClickAction("Remove 18 first rows", "Body rows",
+ new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ for (int i = 0; i < 18; i++) {
+ Object firstItemId = ds.getIdByIndex(0);
+ ds.removeItem(firstItemId);
+ }
+ }
+ }, null);
+
+ createClickAction("Modify first row (getItemProperty)", "Body rows",
+ new Command<Grid, String>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ Object firstItemId = ds.getIdByIndex(0);
+ Item item = ds.getItem(firstItemId);
+ for (int i = 0; i < COLUMNS; i++) {
+ Property<?> property = item
+ .getItemProperty(getColumnProperty(i));
+ if (property.getType().equals(String.class)) {
+ ((Property<String>) property)
+ .setValue("modified: " + i);
+ }
+ }
+ }
+ }, null);
+
+ createClickAction("Modify first row (getContainerProperty)",
+ "Body rows", new Command<Grid, String>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ Object firstItemId = ds.getIdByIndex(0);
+ for (Object containerPropertyId : ds
+ .getContainerPropertyIds()) {
+ Property<?> property = ds.getContainerProperty(
+ firstItemId, containerPropertyId);
+ if (property.getType().equals(String.class)) {
+ ((Property<String>) property)
+ .setValue("modified: "
+ + containerPropertyId);
+ }
+ }
+ }
+ }, null);
+
+ createBooleanAction("Select first row", "Body rows", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ public void execute(Grid grid, Boolean select, Object data) {
+ final Object firstItemId = grid
+ .getContainerDataSource().firstItemId();
+ if (select.booleanValue()) {
+ grid.select(firstItemId);
+ } else {
+ grid.deselect(firstItemId);
+ }
+ }
+ });
+
+ createClickAction("Remove all rows", "Body rows",
+ new Command<Grid, String>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ ds.removeAllItems();
+ }
+ }, null);
+ }
+
+ protected void createEditorActions() {
+ createBooleanAction("Enabled", "Editor", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ public void execute(Grid c, Boolean value, Object data) {
+ c.setEditorEnabled(value);
+ }
+ });
+
+ createClickAction("Edit item 5", "Editor", new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ c.editItem(5);
+ }
+ }, null);
+
+ createClickAction("Edit item 100", "Editor",
+ new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ c.editItem(100);
+ }
+ }, null);
+ createClickAction("Save", "Editor", new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ try {
+ c.saveEditor();
+ } catch (CommitException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }, null);
+ createClickAction("Cancel edit", "Editor", new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ c.cancelEditor();
+ }
+ }, null);
+ }
+
+ @SuppressWarnings("boxing")
+ protected void addHeightActions() {
+ createCategory("Height by Rows", "Size");
+
+ createBooleanAction("HeightMode Row", "Size", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ public void execute(Grid c, Boolean heightModeByRows,
+ Object data) {
+ c.setHeightMode(heightModeByRows ? HeightMode.ROW
+ : HeightMode.CSS);
+ }
+ }, null);
+
+ addActionForHeightByRows(1d / 3d);
+ addActionForHeightByRows(2d / 3d);
+
+ for (double i = 1; i < 5; i++) {
+ addActionForHeightByRows(i);
+ addActionForHeightByRows(i + 1d / 3d);
+ addActionForHeightByRows(i + 2d / 3d);
+ }
+
+ Command<Grid, String> sizeCommand = new Command<Grid, String>() {
+ @Override
+ public void execute(Grid grid, String height, Object data) {
+ grid.setHeight(height);
+ }
+ };
+
+ createCategory("Height", "Size");
+ // header 20px + scrollbar 16px = 36px baseline
+ createClickAction("86px (no drag scroll select)", "Height",
+ sizeCommand, "86px");
+ createClickAction("96px (drag scroll select limit)", "Height",
+ sizeCommand, "96px");
+ createClickAction("106px (drag scroll select enabled)", "Height",
+ sizeCommand, "106px");
+ }
+
+ private void addActionForHeightByRows(final Double i) {
+ DecimalFormat df = new DecimalFormat("0.00");
+ createClickAction(df.format(i) + " rows", "Height by Rows",
+ new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ c.setHeightByRows(i);
+ }
+ }, null);
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 12829;
+ }
+
+ @Override
+ protected Class<Grid> getTestClass() {
+ return Grid.class;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java
new file mode 100644
index 0000000000..0e339ec0ae
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public abstract class GridBasicFeaturesTest extends MultiBrowserTest {
+
+ @Override
+ protected DesiredCapabilities getDesiredCapabilities() {
+ DesiredCapabilities dCap = super.getDesiredCapabilities();
+ if (BrowserUtil.isIE(dCap)) {
+ dCap.setCapability("requireWindowFocus", true);
+ }
+ return super.getDesiredCapabilities();
+ }
+
+ @Override
+ protected Class<?> getUIClass() {
+ return GridBasicFeatures.class;
+ }
+
+ protected void selectSubMenu(String menuCaption) {
+ selectMenu(menuCaption);
+ new Actions(getDriver()).moveByOffset(100, 0).build().perform();
+ }
+
+ protected void selectMenu(String menuCaption) {
+ getDriver().findElement(
+ By.xpath("//span[text() = '" + menuCaption + "']")).click();
+ }
+
+ protected void selectMenuPath(String... menuCaptions) {
+ selectMenu(menuCaptions[0]);
+ for (int i = 1; i < menuCaptions.length; i++) {
+ selectSubMenu(menuCaptions[i]);
+ }
+ }
+
+ protected GridElement getGridElement() {
+ return ((TestBenchElement) findElement(By.id("testComponent")))
+ .wrap(GridElement.class);
+ }
+
+ protected void scrollGridVerticallyTo(double px) {
+ executeScript("arguments[0].scrollTop = " + px,
+ getGridVerticalScrollbar());
+ }
+
+ protected int getGridVerticalScrollPos() {
+ return ((Number) executeScript("return arguments[0].scrollTop",
+ getGridVerticalScrollbar())).intValue();
+ }
+
+ protected List<TestBenchElement> getGridHeaderRowCells() {
+ List<TestBenchElement> headerCells = new ArrayList<TestBenchElement>();
+ for (int i = 0; i < getGridElement().getHeaderCount(); ++i) {
+ headerCells.addAll(getGridElement().getHeaderCells(i));
+ }
+ return headerCells;
+ }
+
+ protected List<TestBenchElement> getGridFooterRowCells() {
+ List<TestBenchElement> footerCells = new ArrayList<TestBenchElement>();
+ for (int i = 0; i < getGridElement().getFooterCount(); ++i) {
+ footerCells.addAll(getGridElement().getFooterCells(i));
+ }
+ return footerCells;
+ }
+
+ protected WebElement getEditor() {
+ List<WebElement> elems = getGridElement().findElements(
+ By.className("v-grid-editor"));
+
+ assertLessThanOrEqual("number of editors", elems.size(), 1);
+
+ return elems.isEmpty() ? null : elems.get(0);
+ }
+
+ private Object executeScript(String script, WebElement element) {
+ final WebDriver driver = getDriver();
+ if (driver instanceof JavascriptExecutor) {
+ final JavascriptExecutor je = (JavascriptExecutor) driver;
+ return je.executeScript(script, element);
+ } else {
+ throw new IllegalStateException("current driver "
+ + getDriver().getClass().getName() + " is not a "
+ + JavascriptExecutor.class.getSimpleName());
+ }
+ }
+
+ protected WebElement getGridVerticalScrollbar() {
+ return getDriver()
+ .findElement(
+ By.xpath("//div[contains(@class, \"v-grid-scroller-vertical\")]"));
+ }
+
+ /**
+ * Reloads the page without restartApplication. This occasionally breaks
+ * stuff.
+ */
+ protected void reopenTestURL() {
+ String testUrl = getTestUrl();
+ testUrl = testUrl.replace("?restartApplication", "?");
+ testUrl = testUrl.replace("?&", "?");
+ driver.get(testUrl);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesValo.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesValo.java
new file mode 100644
index 0000000000..aef353fe93
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesValo.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.ui.themes.ValoTheme;
+
+@Theme(ValoTheme.THEME_NAME)
+public class GridBasicFeaturesValo extends GridBasicFeatures {
+ @Override
+ @Deprecated
+ public String getTheme() {
+ return ValoTheme.THEME_NAME;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java
new file mode 100644
index 0000000000..3f84d40b01
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridClientDataSourcesWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+import com.vaadin.ui.UI;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class GridClientDataSources extends UI {
+
+ @Override
+ protected void init(VaadinRequest request) {
+ setContent(new TestWidgetComponent(GridClientDataSourcesWidget.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java
new file mode 100644
index 0000000000..30d6541344
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridClientDataSourcesTest extends MultiBrowserTest {
+
+ @Before
+ public void before() {
+ openTestURL();
+ }
+
+ @Test
+ public void normalRestishDatasource() throws Exception {
+ selectMenuPath("DataSources", "RESTish", "Use");
+ assertCellPresent("cell 0 #0");
+
+ scrollToBottom();
+ assertCellPresent("cell 199 #0");
+ assertCellNotPresent("cell 200 #0");
+ }
+
+ @Test
+ public void growOnRequestRestishDatasource() throws Exception {
+ selectMenuPath("DataSources", "RESTish", "Use");
+ selectMenuPath("DataSources", "RESTish", "Next request +10");
+
+ scrollToBottom();
+ /* second scroll needed because of scrollsize change after scrolling */
+ scrollToBottom();
+
+ assertCellPresent("cell 209 #1");
+ assertCellNotPresent("cell 210 #1");
+ }
+
+ @Test
+ public void shrinkOnRequestRestishDatasource() throws Exception {
+ selectMenuPath("DataSources", "RESTish", "Use");
+ scrollToBottom();
+
+ selectMenuPath("DataSources", "RESTish", "Next request -10");
+ scrollToTop();
+
+ assertCellPresent("cell 0 #1");
+ }
+
+ @Test
+ public void pushChangeRestishDatasource() throws Exception {
+ selectMenuPath("DataSources", "RESTish", "Use");
+ selectMenuPath("DataSources", "RESTish", "Push data change");
+ assertCellPresent("cell 0 #1");
+ assertCellNotPresent("cell 0 #0");
+ }
+
+ @Test
+ public void growOnPushRestishDatasource() throws Exception {
+ selectMenuPath("DataSources", "RESTish", "Use");
+ selectMenuPath("DataSources", "RESTish", "Push data change +10");
+ assertCellPresent("cell 0 #1");
+ assertCellNotPresent("cell 0 #0");
+ scrollToBottom();
+ assertCellPresent("cell 209 #1");
+ }
+
+ @Test
+ public void shrinkOnPushRestishDatasource() throws Exception {
+ selectMenuPath("DataSources", "RESTish", "Use");
+ scrollToBottom();
+
+ selectMenuPath("DataSources", "RESTish", "Push data change -10");
+ assertCellPresent("cell 189 #1");
+ assertCellNotPresent("cell 189 #0");
+ assertCellNotPresent("cell 199 #1");
+ assertCellNotPresent("cell 199 #0");
+ }
+
+ private void assertCellPresent(String content) {
+ assertNotNull("A cell with content \"" + content
+ + "\" should've been found", findByXPath("//td[text()='"
+ + content + "']"));
+ }
+
+ private void assertCellNotPresent(String content) {
+ assertNull("A cell with content \"" + content
+ + "\" should've not been found", findByXPath("//td[text()='"
+ + content + "']"));
+ }
+
+ private void scrollToTop() {
+ scrollVerticallyTo(0);
+ }
+
+ private void scrollToBottom() {
+ scrollVerticallyTo(9999);
+ }
+
+ private WebElement findByXPath(String string) {
+ if (isElementPresent(By.xpath(string))) {
+ return findElement(By.xpath(string));
+ } else {
+ return null;
+ }
+ }
+
+ private void scrollVerticallyTo(int px) {
+ executeScript("arguments[0].scrollTop = " + px, findVerticalScrollbar());
+ }
+
+ private Object executeScript(String script, Object args) {
+ final WebDriver driver = getDriver();
+ if (driver instanceof JavascriptExecutor) {
+ final JavascriptExecutor je = (JavascriptExecutor) driver;
+ return je.executeScript(script, args);
+ } else {
+ throw new IllegalStateException("current driver "
+ + getDriver().getClass().getName() + " is not a "
+ + JavascriptExecutor.class.getSimpleName());
+ }
+ }
+
+ private WebElement findVerticalScrollbar() {
+ return getDriver().findElement(
+ By.xpath("//div[contains(@class, "
+ + "\"v-grid-scroller-vertical\")]"));
+ }
+
+ private void selectMenu(String menuCaption) {
+ WebElement menuElement = getMenuElement(menuCaption);
+ Dimension size = menuElement.getSize();
+ new Actions(getDriver()).moveToElement(menuElement, size.width - 10,
+ size.height / 2).perform();
+ }
+
+ private WebElement getMenuElement(String menuCaption) {
+ return getDriver().findElement(
+ By.xpath("//td[text() = '" + menuCaption + "']"));
+ }
+
+ private void selectMenuPath(String... menuCaptions) {
+ new Actions(getDriver()).moveToElement(getMenuElement(menuCaptions[0]))
+ .click().perform();
+ for (int i = 1; i < menuCaptions.length - 1; ++i) {
+ selectMenu(menuCaptions[i]);
+ new Actions(getDriver()).moveByOffset(20, 0).perform();
+ }
+ new Actions(getDriver())
+ .moveToElement(
+ getMenuElement(menuCaptions[menuCaptions.length - 1]))
+ .click().perform();
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInit.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInit.java
new file mode 100644
index 0000000000..afbe3fcbbb
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInit.java
@@ -0,0 +1,20 @@
+package com.vaadin.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.annotations.Title;
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridHeightByRowOnInitWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+import com.vaadin.ui.UI;
+
+@Theme("valo")
+@Title("Client Grid height by row on init")
+@Widgetset(TestingWidgetSet.NAME)
+public class GridClientHeightByRowOnInit extends UI {
+ @Override
+ protected void init(VaadinRequest request) {
+ setContent(new TestWidgetComponent(GridHeightByRowOnInitWidget.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInitTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInitTest.java
new file mode 100644
index 0000000000..dadaff0eaa
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientHeightByRowOnInitTest.java
@@ -0,0 +1,19 @@
+package com.vaadin.tests.components.grid.basicfeatures;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@SuppressWarnings("all")
+@TestCategory("grid")
+public class GridClientHeightByRowOnInitTest extends MultiBrowserTest {
+ @Test
+ public void gridHeightIsMoreThanACoupleOfRows() {
+ openTestURL();
+ int height = findElement(By.className("v-grid")).getSize().getHeight();
+ assertGreater("Grid should be much taller than 150px (was " + height
+ + "px)", height, 150);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRenderer.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRenderer.java
new file mode 100644
index 0000000000..5c4ccfae89
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRenderer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridDefaultTextRendererWidget;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+import com.vaadin.ui.UI;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class GridDefaultTextRenderer extends UI {
+
+ @Override
+ protected void init(VaadinRequest request) {
+ setContent(new TestWidgetComponent(GridDefaultTextRendererWidget.class));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRendererTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRendererTest.java
new file mode 100644
index 0000000000..79eadd03d8
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridDefaultTextRendererTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.testbench.elements.ServerClass;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridDefaultTextRendererTest extends MultiBrowserTest {
+
+ @ServerClass("com.vaadin.tests.widgetset.server.TestWidgetComponent")
+ public static class MyGridElement extends GridElement {
+ // empty
+ }
+
+ private GridElement grid;
+
+ @Before
+ public void init() {
+ setDebug(true);
+ openTestURL();
+ grid = $(MyGridElement.class).first();
+ assertFalse("There was an unexpected notification during init",
+ $(NotificationElement.class).exists());
+ }
+
+ @Test
+ public void testNullIsRenderedAsEmptyStringByDefaultTextRenderer() {
+ assertTrue("First cell should've been empty", grid.getCell(0, 0)
+ .getText().isEmpty());
+ }
+
+ @Test
+ public void testStringIsRenderedAsStringByDefaultTextRenderer() {
+ assertEquals("Second cell should've been populated ", "string", grid
+ .getCell(1, 0).getText());
+ }
+
+ @Test
+ public void testWarningShouldNotBeInDebugLog() {
+ assertFalse("Warning visible with string content.",
+ isElementPresent(By.xpath("//span[contains(.,'attached:#1')]")));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInit.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInit.java
new file mode 100644
index 0000000000..0b6e6c36dd
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInit.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.annotations.Title;
+import com.vaadin.data.Container;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.themes.ValoTheme;
+
+@Title("Server Grid height by row on init")
+@Theme(ValoTheme.THEME_NAME)
+public class GridHeightByRowOnInit extends UI {
+
+ private static final String PROPERTY = "Property";
+
+ @Override
+ protected void init(VaadinRequest request) {
+ final Grid grid = new Grid();
+ Container.Indexed container = grid.getContainerDataSource();
+ container.addContainerProperty(PROPERTY, String.class, "");
+
+ container.addItem("A").getItemProperty(PROPERTY).setValue("A");
+ container.addItem("B").getItemProperty(PROPERTY).setValue("B");
+ container.addItem("C").getItemProperty(PROPERTY).setValue("C");
+ container.addItem("D").getItemProperty(PROPERTY).setValue("D");
+ container.addItem("E").getItemProperty(PROPERTY).setValue("E");
+
+ grid.setHeightMode(HeightMode.ROW);
+ grid.setHeightByRows(5);
+
+ setContent(grid);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInitTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInitTest.java
new file mode 100644
index 0000000000..15a1cd6c85
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridHeightByRowOnInitTest.java
@@ -0,0 +1,20 @@
+package com.vaadin.tests.components.grid.basicfeatures;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@SuppressWarnings("all")
+@TestCategory("grid")
+public class GridHeightByRowOnInitTest extends MultiBrowserTest {
+
+ @Test
+ public void gridHeightIsMoreThanACoupleOfRows() {
+ openTestURL();
+ int height = $(GridElement.class).first().getSize().getHeight();
+ assertGreater("Grid should be much taller than 150px (was " + height
+ + "px)", height, 150);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicators.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicators.java
new file mode 100644
index 0000000000..6d602baf06
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicators.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Grid;
+
+public class GridSortingIndicators extends AbstractTestUI {
+
+ private static int FOO_MIN = 4;
+ private static int BAR_MULTIPLIER = 3;
+ private static int BAZ_MAX = 132;
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ final Grid grid = new Grid(createContainer());
+ addComponent(grid);
+ grid.sort(Sort.by("foo").then("bar", SortDirection.DESCENDING)
+ .then("baz"));
+
+ addComponent(new Button("Reverse sorting", new Button.ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.sort(Sort.by("baz", SortDirection.DESCENDING).then("bar")
+ .then("foo", SortDirection.DESCENDING));
+ }
+ }));
+ }
+
+ private Container.Indexed createContainer() {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty("foo", Integer.class, 0);
+ container.addContainerProperty("bar", Integer.class, 0);
+ container.addContainerProperty("baz", Integer.class, 0);
+ for (int i = 0; i < 10; ++i) {
+ Item item = container.getItem(container.addItem());
+ item.getItemProperty("foo").setValue(FOO_MIN + i);
+ item.getItemProperty("baz").setValue(BAZ_MAX - i);
+ item.getItemProperty("bar").setValue(BAR_MULTIPLIER * i);
+ }
+ return container;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicatorsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicatorsTest.java
new file mode 100644
index 0000000000..6a5360f152
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSortingIndicatorsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridSortingIndicatorsTest extends MultiBrowserTest {
+
+ @Test
+ public void testSortingIndicators() throws IOException {
+ openTestURL();
+ compareScreen("initialSort");
+
+ $(ButtonElement.class).first().click();
+
+ compareScreen("reversedSort");
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/DisabledGridClientTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/DisabledGridClientTest.java
new file mode 100644
index 0000000000..63d031bc85
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/DisabledGridClientTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement.GridRowElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+public class DisabledGridClientTest extends GridBasicClientFeaturesTest {
+
+ @Before
+ public void setUp() {
+ openTestURL();
+ selectMenuPath("Component", "State", "Enabled");
+ }
+
+ @Test
+ public void testSelection() {
+ selectMenuPath("Component", "State", "Selection mode", "single");
+
+ GridRowElement row = getGridElement().getRow(0);
+ row.click();
+ assertFalse("disabled row should not be selected", row.isSelected());
+
+ }
+
+ @Test
+ public void testEditorOpening() {
+ selectMenuPath("Component", "Editor", "Enabled");
+
+ GridRowElement row = getGridElement().getRow(0);
+ row.click();
+ assertNull("Editor should not open", getEditor());
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+ assertNull("Editor should not open", getEditor());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridCellStyleGeneratorTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridCellStyleGeneratorTest.java
new file mode 100644
index 0000000000..8188553e61
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridCellStyleGeneratorTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.GridElement.GridRowElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget;
+
+public class GridCellStyleGeneratorTest extends GridBasicClientFeaturesTest {
+
+ @Test
+ public void testStyleNameGeneratorScrolling() throws Exception {
+ openTestURL();
+
+ selectCellStyleNameGenerator(GridBasicClientFeaturesWidget.CELL_STYLE_GENERATOR_COL_INDEX);
+ selectRowStyleNameGenerator(GridBasicClientFeaturesWidget.ROW_STYLE_GENERATOR_ROW_INDEX);
+
+ GridRowElement row2 = getGridElement().getRow(2);
+ GridCellElement cell4_2 = getGridElement().getCell(4, 2);
+
+ Assert.assertTrue(hasCssClass(row2, "2"));
+ Assert.assertTrue(hasCssClass(cell4_2, "4_2"));
+
+ // Scroll down and verify that the old elements don't have the
+ // stylename any more
+
+ getGridElement().getRow(350);
+
+ Assert.assertFalse(hasCssClass(row2, "2"));
+ Assert.assertFalse(hasCssClass(cell4_2, "4_2"));
+ }
+
+ @Test
+ public void testDisableStyleNameGenerator() throws Exception {
+ openTestURL();
+
+ selectCellStyleNameGenerator(GridBasicClientFeaturesWidget.CELL_STYLE_GENERATOR_COL_INDEX);
+ selectRowStyleNameGenerator(GridBasicClientFeaturesWidget.ROW_STYLE_GENERATOR_ROW_INDEX);
+
+ // Just verify that change was effective
+ GridRowElement row2 = getGridElement().getRow(2);
+ GridCellElement cell4_2 = getGridElement().getCell(4, 2);
+
+ Assert.assertTrue(hasCssClass(row2, "2"));
+ Assert.assertTrue(hasCssClass(cell4_2, "4_2"));
+
+ // Disable the generator and check again
+ selectCellStyleNameGenerator(GridBasicClientFeaturesWidget.CELL_STYLE_GENERATOR_NONE);
+ selectRowStyleNameGenerator(GridBasicClientFeaturesWidget.ROW_STYLE_GENERATOR_NONE);
+
+ Assert.assertFalse(hasCssClass(row2, "2"));
+ Assert.assertFalse(hasCssClass(cell4_2, "4_2"));
+ }
+
+ @Test
+ public void testChangeStyleNameGenerator() throws Exception {
+ openTestURL();
+
+ selectCellStyleNameGenerator(GridBasicClientFeaturesWidget.CELL_STYLE_GENERATOR_COL_INDEX);
+ selectRowStyleNameGenerator(GridBasicClientFeaturesWidget.ROW_STYLE_GENERATOR_ROW_INDEX);
+
+ // Just verify that change was effective
+ GridRowElement row2 = getGridElement().getRow(2);
+ GridCellElement cell4_2 = getGridElement().getCell(4, 2);
+
+ Assert.assertTrue(hasCssClass(row2, "2"));
+ Assert.assertTrue(hasCssClass(cell4_2, "4_2"));
+
+ // Change the generator and check again
+ selectRowStyleNameGenerator(GridBasicClientFeaturesWidget.ROW_STYLE_GENERATOR_NONE);
+ selectCellStyleNameGenerator(GridBasicClientFeaturesWidget.CELL_STYLE_GENERATOR_SIMPLE);
+
+ // Old styles removed?
+ Assert.assertFalse(hasCssClass(row2, "2"));
+ Assert.assertFalse(hasCssClass(cell4_2, "4_2"));
+
+ // New style present?
+ Assert.assertTrue(hasCssClass(cell4_2, "two"));
+ }
+
+ @Test
+ public void testStyleNameGeneratorChangePrimary() throws Exception {
+ openTestURL();
+
+ selectCellStyleNameGenerator(GridBasicClientFeaturesWidget.CELL_STYLE_GENERATOR_COL_INDEX);
+ selectRowStyleNameGenerator(GridBasicClientFeaturesWidget.ROW_STYLE_GENERATOR_ROW_INDEX);
+
+ // Just verify that change was effective
+ GridRowElement row2 = getGridElement().getRow(2);
+ GridCellElement cell4_2 = getGridElement().getCell(4, 2);
+
+ Assert.assertTrue(hasCssClass(row2, "2"));
+ Assert.assertTrue(hasCssClass(cell4_2, "4_2"));
+
+ // Change primary stylename
+ selectMenuPath("Component", "State", "Primary Stylename", "v-escalator");
+
+ // Styles still present
+ Assert.assertTrue(hasCssClass(row2, "2"));
+ Assert.assertTrue(hasCssClass(cell4_2, "4_2"));
+
+ // New styles present?
+ Assert.assertFalse(hasCssClass(row2, "v-escalator-row-2"));
+ Assert.assertFalse(hasCssClass(cell4_2, "v-escalator-cell-content-4_2"));
+ }
+
+ private void selectCellStyleNameGenerator(String name) {
+ selectMenuPath("Component", "State", "Cell style generator", name);
+ }
+
+ private void selectRowStyleNameGenerator(String name) {
+ selectMenuPath("Component", "State", "Row style generator", name);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java
new file mode 100644
index 0000000000..82bf349096
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget;
+
+public class GridClientColumnPropertiesTest extends GridBasicClientFeaturesTest {
+
+ @Test
+ public void initialColumnWidths() {
+ openTestURL();
+
+ for (int col = 0; col < GridBasicClientFeaturesWidget.COLUMNS; col++) {
+ int width = getGridElement().getCell(0, col).getSize().getWidth();
+ if (col <= 6) {
+ // Growing column widths
+ int expectedWidth = 50 + col * 25;
+ assertEquals("column " + col + " has incorrect width",
+ expectedWidth, width);
+ }
+ }
+ }
+
+ @Test
+ public void testChangingColumnWidth() {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Width", "50px");
+ int width = getGridElement().getCell(0, 0).getSize().getWidth();
+ assertEquals(50, width);
+
+ selectMenuPath("Component", "Columns", "Column 0", "Width", "200px");
+ width = getGridElement().getCell(0, 0).getSize().getWidth();
+ assertEquals(200, width);
+
+ selectMenuPath("Component", "Columns", "Column 0", "Width", "auto");
+ int autoWidth = getGridElement().getCell(0, 0).getSize().getWidth();
+ assertLessThan("Automatic sizing should've shrunk the column",
+ autoWidth, width);
+ }
+
+ @Test
+ public void testFrozenColumns() {
+ openTestURL();
+
+ assertFalse(cellIsFrozen(0, 0));
+ assertFalse(cellIsFrozen(0, 1));
+
+ selectMenuPath("Component", "State", "Frozen column count", "1 columns");
+
+ assertTrue(cellIsFrozen(1, 0));
+ assertFalse(cellIsFrozen(1, 1));
+
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+
+ assertTrue(cellIsFrozen(1, 1));
+ assertFalse(cellIsFrozen(1, 2));
+
+ selectMenuPath("Component", "State", "Frozen column count", "0 columns");
+
+ assertTrue(cellIsFrozen(1, 0));
+ assertFalse(cellIsFrozen(1, 1));
+
+ selectMenuPath("Component", "State", "Frozen column count",
+ "-1 columns");
+
+ assertFalse(cellIsFrozen(1, 0));
+ }
+
+ @Test
+ public void testBrokenRenderer() {
+ setDebug(true);
+ openTestURL();
+
+ GridElement gridElement = getGridElement();
+
+ // Scroll first row out of view
+ gridElement.getRow(50);
+
+ // Enable broken renderer for the first row
+ selectMenuPath("Component", "Columns", "Column 0", "Broken renderer");
+
+ // Shouldn't have an error notification yet
+ assertFalse("Notification was present",
+ isElementPresent(NotificationElement.class));
+
+ // Scroll broken row into view and enjoy the chaos
+ gridElement.getRow(0);
+
+ assertTrue("Notification was not present",
+ isElementPresent(NotificationElement.class));
+
+ assertFalse("Text in broken cell should have old value",
+ "(0, 0)".equals(gridElement.getCell(0, 0).getText()));
+
+ assertEquals("Neighbour cell should be updated", "(0, 1)", gridElement
+ .getCell(0, 1).getText());
+
+ assertEquals("Neighbour cell should be updated", "(1, 0)", gridElement
+ .getCell(1, 0).getText());
+ }
+
+ private boolean cellIsFrozen(int row, int col) {
+ return getGridElement().getCell(row, col).isFrozen();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeEditorTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeEditorTest.java
new file mode 100644
index 0000000000..29e6fed68c
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeEditorTest.java
@@ -0,0 +1,13 @@
+package com.vaadin.tests.components.grid.basicfeatures.client;
+
+import org.junit.Before;
+
+public class GridClientCompositeEditorTest extends GridEditorClientTest {
+
+ @Override
+ @Before
+ public void setUp() {
+ setUseComposite(true);
+ super.setUp();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeFooterTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeFooterTest.java
new file mode 100644
index 0000000000..26ae7320b0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeFooterTest.java
@@ -0,0 +1,11 @@
+package com.vaadin.tests.components.grid.basicfeatures.client;
+
+import org.junit.Before;
+
+public class GridClientCompositeFooterTest extends GridFooterTest {
+
+ @Before
+ public void setUp() {
+ setUseComposite(true);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeHeaderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeHeaderTest.java
new file mode 100644
index 0000000000..c0ed833ab2
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeHeaderTest.java
@@ -0,0 +1,11 @@
+package com.vaadin.tests.components.grid.basicfeatures.client;
+
+import org.junit.Before;
+
+public class GridClientCompositeHeaderTest extends GridHeaderTest {
+
+ @Before
+ public void setUp() {
+ setUseComposite(true);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeKeyEventsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeKeyEventsTest.java
new file mode 100644
index 0000000000..a09a31830f
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeKeyEventsTest.java
@@ -0,0 +1,12 @@
+package com.vaadin.tests.components.grid.basicfeatures.client;
+
+import org.junit.Before;
+
+public class GridClientCompositeKeyEventsTest extends
+ GridClientKeyEventsTest {
+
+ @Before
+ public void setUp() {
+ setUseComposite(true);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeSelectionTest.java
new file mode 100644
index 0000000000..7a79a114b8
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientCompositeSelectionTest.java
@@ -0,0 +1,11 @@
+package com.vaadin.tests.components.grid.basicfeatures.client;
+
+import org.junit.Before;
+
+public class GridClientCompositeSelectionTest extends GridClientSelectionTest {
+
+ @Before
+ public void setUp() {
+ setUseComposite(true);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java
new file mode 100644
index 0000000000..dc4dedd3a0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+public class GridClientKeyEventsTest extends GridBasicClientFeaturesTest {
+
+ private List<String> eventOrder = Arrays.asList("Down", "Up", "Press");
+
+ @Test
+ public void testBodyKeyEvents() throws IOException {
+ openTestURL();
+
+ getGridElement().getCell(2, 2).click();
+
+ new Actions(getDriver()).sendKeys("a").perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals("Body key event handler was not called.",
+ "(2, 2) event: GridKey" + eventOrder.get(i) + "Event:["
+ + (eventOrder.get(i).equals("Press") ? "a" : 65)
+ + "]",
+ findElements(By.className("v-label")).get(i * 3).getText());
+
+ assertTrue("Header key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText().isEmpty());
+ assertTrue("Footer key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 2)
+ .getText().isEmpty());
+ }
+
+ }
+
+ @Test
+ public void testHeaderKeyEvents() throws IOException {
+ openTestURL();
+
+ getGridElement().getHeaderCell(0, 2).click();
+
+ new Actions(getDriver()).sendKeys("a").perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals("Header key event handler was not called.",
+ "(0, 2) event: GridKey" + eventOrder.get(i) + "Event:["
+ + (eventOrder.get(i).equals("Press") ? "a" : 65)
+ + "]",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText());
+
+ assertTrue("Body key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3).getText()
+ .isEmpty());
+ assertTrue("Footer key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 2)
+ .getText().isEmpty());
+ }
+ }
+
+ @Test
+ public void testFooterKeyEvents() throws IOException {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+ getGridElement().getFooterCell(0, 2).click();
+
+ new Actions(getDriver()).sendKeys("a").perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals("Footer key event handler was not called.",
+ "(0, 2) event: GridKey" + eventOrder.get(i) + "Event:["
+ + (eventOrder.get(i).equals("Press") ? "a" : 65)
+ + "]",
+ findElements(By.className("v-label")).get(i * 3 + 2)
+ .getText());
+
+ assertTrue("Body key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3).getText()
+ .isEmpty());
+ assertTrue("Header key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText().isEmpty());
+
+ }
+ }
+
+ @Test
+ public void testNoKeyEventsFromWidget() {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 2", "Header Type",
+ "Widget Header");
+ GridCellElement header = getGridElement().getHeaderCell(0, 2);
+ header.findElement(By.tagName("button")).click();
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertTrue("Header key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText().isEmpty());
+
+ }
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java
new file mode 100644
index 0000000000..d4c10da626
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+public class GridClientSelectionTest extends GridBasicClientFeaturesTest {
+
+ @Test
+ public void testChangeSelectionMode() {
+ openTestURL();
+
+ setSelectionModelNone();
+ assertTrue("First column was selection column", getGridElement()
+ .getCell(0, 0).getText().equals("(0, 0)"));
+ setSelectionModelMulti();
+ assertTrue("First column was not selection column", getGridElement()
+ .getCell(0, 1).getText().equals("(0, 0)"));
+ }
+
+ @Test
+ public void testSelectAllCheckbox() {
+ openTestURL();
+
+ setSelectionModelMulti();
+ selectMenuPath("Component", "DataSource", "Reset with 100 rows of Data");
+ GridCellElement header = getGridElement().getHeaderCell(0, 0);
+
+ assertTrue("No checkbox", header.isElementPresent(By.tagName("input")));
+ header.findElement(By.tagName("input")).click();
+
+ for (int i = 0; i < 100; i += 10) {
+ assertTrue("Row " + i + " was not selected.", getGridElement()
+ .getRow(i).isSelected());
+ }
+
+ header.findElement(By.tagName("input")).click();
+ assertFalse("Row 52 was still selected", getGridElement().getRow(52)
+ .isSelected());
+ }
+
+ @Test
+ public void testSelectAllCheckboxWhenChangingModels() {
+ openTestURL();
+
+ GridCellElement header;
+ header = getGridElement().getHeaderCell(0, 0);
+ assertFalse(
+ "Check box shouldn't have been in header for None Selection Model",
+ header.isElementPresent(By.tagName("input")));
+
+ setSelectionModelMulti();
+ header = getGridElement().getHeaderCell(0, 0);
+ assertTrue("Multi Selection Model should have select all checkbox",
+ header.isElementPresent(By.tagName("input")));
+
+ setSelectionModelSingle();
+ header = getGridElement().getHeaderCell(0, 0);
+ assertFalse(
+ "Check box shouldn't have been in header for Single Selection Model",
+ header.isElementPresent(By.tagName("input")));
+
+ setSelectionModelNone();
+ header = getGridElement().getHeaderCell(0, 0);
+ assertFalse(
+ "Check box shouldn't have been in header for None Selection Model",
+ header.isElementPresent(By.tagName("input")));
+
+ }
+
+ private void setSelectionModelMulti() {
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+ }
+
+ private void setSelectionModelSingle() {
+ selectMenuPath("Component", "State", "Selection mode", "single");
+ }
+
+ private void setSelectionModelNone() {
+ selectMenuPath("Component", "State", "Selection mode", "none");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientStructureTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientStructureTest.java
new file mode 100644
index 0000000000..74cf368da9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientStructureTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+@SuppressWarnings("all")
+public class GridClientStructureTest extends GridBasicClientFeaturesTest {
+ @Test
+ public void haederDecoSizeShouldBeRecalculated() {
+ // it's easier to notice with valo
+ openTestURL("theme=valo");
+
+ WebElement topDeco = getGridElement().findElement(
+ By.className("v-grid-header-deco"));
+ assertGreater(
+ "The header deco in Valo hasn't been recalculated after initial rendering",
+ topDeco.getSize().getHeight(), 20);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridEditorClientTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridEditorClientTest.java
new file mode 100644
index 0000000000..a67b901198
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridEditorClientTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridEditorClientTest extends GridBasicClientFeaturesTest {
+
+ @Before
+ public void setUp() {
+ openTestURL();
+ selectMenuPath("Component", "Editor", "Enabled");
+ }
+
+ @Test
+ public void testProgrammaticOpeningClosing() {
+ selectMenuPath("Component", "Editor", "Edit row 5");
+ assertNotNull(getEditor());
+
+ selectMenuPath("Component", "Editor", "Cancel edit");
+ assertNull(getEditor());
+ assertEquals("Row 5 edit cancelled",
+ findElement(By.className("grid-editor-log")).getText());
+ }
+
+ @Test
+ public void testProgrammaticOpeningWithScroll() {
+ selectMenuPath("Component", "Editor", "Edit row 100");
+ assertNotNull(getEditor());
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testVerticalScrollLocking() {
+ selectMenuPath("Component", "Editor", "Edit row 5");
+ getGridElement().getCell(200, 0);
+ }
+
+ @Test
+ public void testKeyboardOpeningClosing() {
+
+ getGridElement().getCell(4, 0).click();
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ assertNotNull(getEditor());
+
+ new Actions(getDriver()).sendKeys(Keys.ESCAPE).perform();
+ assertNull(getEditor());
+ assertEquals("Row 4 edit cancelled",
+ findElement(By.className("grid-editor-log")).getText());
+
+ // Disable editor
+ selectMenuPath("Component", "Editor", "Enabled");
+
+ getGridElement().getCell(5, 0).click();
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+ assertNull(getEditor());
+ }
+
+ @Test
+ public void testWidgetBinding() throws Exception {
+ selectMenuPath("Component", "Editor", "Edit row 100");
+ WebElement editor = getEditor();
+
+ List<WebElement> widgets = editor.findElements(By
+ .className("gwt-TextBox"));
+
+ assertEquals(GridBasicFeatures.COLUMNS, widgets.size());
+
+ assertEquals("(100, 0)", widgets.get(0).getAttribute("value"));
+ assertEquals("(100, 1)", widgets.get(1).getAttribute("value"));
+ assertEquals("(100, 2)", widgets.get(2).getAttribute("value"));
+
+ assertEquals("100", widgets.get(7).getAttribute("value"));
+ assertEquals("<b>100</b>", widgets.get(9).getAttribute("value"));
+ }
+
+ @Test
+ public void testWithSelectionColumn() throws Exception {
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+ selectMenuPath("Component", "State", "Editor", "Edit row 5");
+
+ WebElement editor = getEditor();
+ List<WebElement> selectorDivs = editor.findElements(By
+ .cssSelector("div"));
+
+ assertTrue("selector column cell should've been empty", selectorDivs
+ .get(0).getAttribute("innerHTML").isEmpty());
+ assertFalse("normal column cell shoul've had contents", selectorDivs
+ .get(1).getAttribute("innerHTML").isEmpty());
+ }
+
+ @Test
+ public void testSave() {
+ selectMenuPath("Component", "Editor", "Edit row 100");
+
+ WebElement textField = getEditor().findElements(
+ By.className("gwt-TextBox")).get(0);
+
+ textField.clear();
+ textField.sendKeys("Changed");
+
+ WebElement saveButton = getEditor().findElement(
+ By.className("v-grid-editor-save"));
+
+ saveButton.click();
+
+ assertEquals("Changed", getGridElement().getCell(100, 0).getText());
+ }
+
+ @Test
+ public void testProgrammaticSave() {
+ selectMenuPath("Component", "Editor", "Edit row 100");
+
+ WebElement textField = getEditor().findElements(
+ By.className("gwt-TextBox")).get(0);
+
+ textField.clear();
+ textField.sendKeys("Changed");
+
+ selectMenuPath("Component", "Editor", "Save");
+
+ assertEquals("Changed", getGridElement().getCell(100, 0).getText());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java
new file mode 100644
index 0000000000..8b65ba315b
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridFooterTest extends GridStaticSectionTest {
+
+ @Test
+ public void testDefaultFooter() {
+ openTestURL();
+
+ // Footer should have zero rows by default
+ assertFooterCount(0);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ assertFooterCount(0);
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ assertFooterCount(0);
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ assertFooterCount(1);
+ }
+
+ @Test
+ public void testAddRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ assertFooterCount(1);
+ assertFooterTexts(0, 0);
+
+ selectMenuPath("Component", "Footer", "Prepend row");
+
+ assertFooterCount(2);
+ assertFooterTexts(1, 0);
+ assertFooterTexts(0, 1);
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ assertFooterCount(3);
+ assertFooterTexts(1, 0);
+ assertFooterTexts(0, 1);
+ assertFooterTexts(2, 2);
+ }
+
+ @Test
+ public void testRemoveRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Prepend row");
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Remove top row");
+
+ assertFooterCount(1);
+ assertFooterTexts(1, 0);
+
+ selectMenuPath("Component", "Footer", "Remove bottom row");
+ assertFooterCount(0);
+ }
+
+ @Test
+ public void joinColumnsByCells() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Row 1", "Join column cells 0, 1");
+
+ GridCellElement spannedCell = getGridElement().getFooterCell(0, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ // TestBench returns the spanned cell for all columns
+ assertEquals(spannedCell.getText(), getGridElement()
+ .getFooterCell(0, 1).getText());
+ }
+
+ @Test
+ public void joinColumnsByColumns() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2");
+
+ GridCellElement spannedCell = getGridElement().getFooterCell(0, 1);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ // TestBench returns the spanned cell for all columns
+ assertEquals(spannedCell.getText(), getGridElement()
+ .getFooterCell(0, 2).getText());
+ }
+
+ @Test
+ public void joinAllColumnsInRow() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Row 1", "Join all columns");
+
+ GridCellElement spannedCell = getGridElement().getFooterCell(0, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("" + GridBasicFeatures.COLUMNS,
+ spannedCell.getAttribute("colspan"));
+
+ for (int columnIndex = 1; columnIndex < GridBasicFeatures.COLUMNS; columnIndex++) {
+ GridCellElement hiddenCell = getGridElement().getFooterCell(0,
+ columnIndex);
+ // TestBench returns the spanned cell for all columns
+ assertEquals(spannedCell.getText(), hiddenCell.getText());
+ }
+ }
+
+ @Test
+ public void testInitialCellTypes() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ GridCellElement textCell = getGridElement().getFooterCell(0, 0);
+ /*
+ * Reindeer has a CSS text transformation that changes the casing so
+ * that we can't rely on it being what we set
+ */
+ assertEquals("footer (0,0)", textCell.getText().toLowerCase());
+
+ GridCellElement widgetCell = getGridElement().getFooterCell(0, 1);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-HTML")));
+
+ GridCellElement htmlCell = getGridElement().getFooterCell(0, 2);
+ assertHTML("<b>Footer (0,2)</b>", htmlCell);
+ }
+
+ @Test
+ public void testDynamicallyChangingCellType() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Columns", "Column 0", "Footer Type",
+ "Widget Footer");
+ GridCellElement widgetCell = getGridElement().getFooterCell(0, 0);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-Button")));
+
+ selectMenuPath("Component", "Columns", "Column 1", "Footer Type",
+ "HTML Footer");
+ GridCellElement htmlCell = getGridElement().getFooterCell(0, 1);
+ assertHTML("<b>HTML Footer</b>", htmlCell);
+
+ selectMenuPath("Component", "Columns", "Column 2", "Footer Type",
+ "Text Footer");
+ GridCellElement textCell = getGridElement().getFooterCell(0, 2);
+
+ /*
+ * Reindeer has a CSS text transformation that changes the casing so
+ * that we can't rely on it being what we set
+ */
+ assertEquals("text footer", textCell.getText().toLowerCase());
+ }
+
+ @Test
+ public void testCellWidgetInteraction() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Columns", "Column 0", "Footer Type",
+ "Widget Footer");
+ GridCellElement widgetCell = getGridElement().getFooterCell(0, 0);
+ WebElement button = widgetCell.findElement(By.className("gwt-Button"));
+
+ assertNotEquals("clicked", button.getText().toLowerCase());
+
+ new Actions(getDriver()).moveToElement(button, 5, 5).click().perform();
+
+ assertEquals("clicked", button.getText().toLowerCase());
+ }
+
+ private void assertFooterCount(int count) {
+ assertEquals("footer count", count, getGridElement().getFooterCount());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java
new file mode 100644
index 0000000000..8cf7f7374f
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridHeaderTest extends GridStaticSectionTest {
+
+ @Test
+ public void testDefaultHeader() throws Exception {
+ openTestURL();
+
+ assertHeaderCount(1);
+ assertHeaderTexts(0, 0);
+ }
+
+ @Test
+ public void testHeaderVisibility() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Visible");
+
+ assertHeaderCount(0);
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ assertHeaderCount(0);
+
+ selectMenuPath("Component", "Header", "Visible");
+
+ assertHeaderCount(2);
+ }
+
+ @Test
+ public void testHeaderCaptions() throws Exception {
+ openTestURL();
+
+ assertHeaderTexts(0, 0);
+ }
+
+ @Test
+ public void testAddRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ assertHeaderCount(2);
+ assertHeaderTexts(0, 0);
+ assertHeaderTexts(1, 1);
+
+ selectMenuPath("Component", "Header", "Prepend row");
+
+ assertHeaderCount(3);
+ assertHeaderTexts(2, 0);
+ assertHeaderTexts(0, 1);
+ assertHeaderTexts(1, 2);
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ assertHeaderCount(4);
+ assertHeaderTexts(2, 0);
+ assertHeaderTexts(0, 1);
+ assertHeaderTexts(1, 2);
+ assertHeaderTexts(3, 3);
+ }
+
+ @Test
+ public void testRemoveRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Prepend row");
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Remove top row");
+
+ assertHeaderCount(2);
+ assertHeaderTexts(0, 0);
+ assertHeaderTexts(2, 1);
+
+ selectMenuPath("Component", "Header", "Remove bottom row");
+ assertHeaderCount(1);
+ assertHeaderTexts(0, 0);
+ }
+
+ @Test
+ public void testDefaultRow() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Sortable");
+
+ GridCellElement headerCell = getGridElement().getHeaderCell(0, 0);
+
+ headerCell.click();
+
+ assertTrue(hasClassName(headerCell, "sort-asc"));
+
+ headerCell.click();
+
+ assertFalse(hasClassName(headerCell, "sort-asc"));
+ assertTrue(hasClassName(headerCell, "sort-desc"));
+
+ selectMenuPath("Component", "Header", "Prepend row");
+ selectMenuPath("Component", "Header", "Default row", "Top");
+
+ assertFalse(hasClassName(headerCell, "sort-desc"));
+ headerCell = getGridElement().getHeaderCell(0, 0);
+ assertTrue(hasClassName(headerCell, "sort-desc"));
+
+ selectMenuPath("Component", "Header", "Default row", "Unset");
+
+ assertFalse(hasClassName(headerCell, "sort-desc"));
+ }
+
+ @Test
+ public void joinHeaderColumnsByCells() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1");
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ // TestBench returns the spanned cell for all spanned columns
+ GridCellElement hiddenCell = getGridElement().getHeaderCell(1, 1);
+ assertEquals(spannedCell.getText(), hiddenCell.getText());
+ }
+
+ @Test
+ public void joinHeaderColumnsByColumns() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 1);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ // TestBench returns the spanned cell for all spanned columns
+ GridCellElement hiddenCell = getGridElement().getHeaderCell(1, 2);
+ assertEquals(spannedCell.getText(), hiddenCell.getText());
+ }
+
+ @Test
+ public void joinAllColumnsInHeaderRow() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join all columns");
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("" + GridBasicFeatures.COLUMNS,
+ spannedCell.getAttribute("colspan"));
+
+ for (int columnIndex = 1; columnIndex < GridBasicFeatures.COLUMNS; columnIndex++) {
+ // TestBench returns the spanned cell for all spanned columns
+ GridCellElement hiddenCell = getGridElement().getHeaderCell(1,
+ columnIndex);
+ assertEquals(spannedCell.getText(), hiddenCell.getText());
+ }
+ }
+
+ @Test
+ public void testInitialCellTypes() throws Exception {
+ openTestURL();
+
+ GridCellElement textCell = getGridElement().getHeaderCell(0, 0);
+
+ /*
+ * Reindeer has a CSS text transformation that changes the casing so
+ * that we can't rely on it being what we set
+ */
+ assertEquals("header (0,0)", textCell.getText().toLowerCase());
+
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 1);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-HTML")));
+
+ GridCellElement htmlCell = getGridElement().getHeaderCell(0, 2);
+ assertHTML("<b>Header (0,2)</b>", htmlCell);
+ }
+
+ @Test
+ public void testDynamicallyChangingCellType() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Header Type",
+ "Widget Header");
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-Button")));
+
+ selectMenuPath("Component", "Columns", "Column 1", "Header Type",
+ "HTML Header");
+ GridCellElement htmlCell = getGridElement().getHeaderCell(0, 1);
+ assertHTML("<b>HTML Header</b>", htmlCell);
+
+ selectMenuPath("Component", "Columns", "Column 2", "Header Type",
+ "Text Header");
+ GridCellElement textCell = getGridElement().getHeaderCell(0, 2);
+
+ /*
+ * Reindeer has a CSS text transformation that changes the casing so
+ * that we can't rely on it being what we set
+ */
+ assertEquals("text header", textCell.getText().toLowerCase());
+ }
+
+ @Test
+ public void testCellWidgetInteraction() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Header Type",
+ "Widget Header");
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0);
+ WebElement button = widgetCell.findElement(By.className("gwt-Button"));
+
+ new Actions(getDriver()).moveToElement(button, 5, 5).click().perform();
+
+ assertEquals("clicked", button.getText().toLowerCase());
+ }
+
+ @Test
+ public void widgetInSortableCellInteraction() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Header Type",
+ "Widget Header");
+
+ selectMenuPath("Component", "Columns", "Column 0", "Sortable");
+
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0);
+ WebElement button = widgetCell.findElement(By.className("gwt-Button"));
+
+ assertNotEquals("clicked", button.getText().toLowerCase());
+
+ new Actions(getDriver()).moveToElement(button, 5, 5).click().perform();
+
+ assertEquals("clicked", button.getText().toLowerCase());
+ }
+
+ private void assertHeaderCount(int count) {
+ assertEquals("header count", count, getGridElement().getHeaderCount());
+ }
+
+ private boolean hasClassName(TestBenchElement element, String name) {
+ return Arrays.asList(element.getAttribute("class").split(" "))
+ .contains(name);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridRowHandleRefreshTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridRowHandleRefreshTest.java
new file mode 100644
index 0000000000..c7a509da45
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridRowHandleRefreshTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+public class GridRowHandleRefreshTest extends GridBasicClientFeaturesTest {
+
+ @Test
+ public void testRefreshingThroughRowHandle() {
+ openTestURL();
+
+ assertEquals("Unexpected initial state", "(0, 0)", getGridElement()
+ .getCell(0, 0).getText());
+ selectMenuPath("Component", "State", "Edit and refresh Row 0");
+ assertEquals("Cell contents did not update correctly", "Foo",
+ getGridElement().getCell(0, 0).getText());
+ }
+
+ @Test
+ public void testDelayedRefreshingThroughRowHandle()
+ throws InterruptedException {
+ openTestURL();
+
+ assertEquals("Unexpected initial state", "(0, 0)", getGridElement()
+ .getCell(0, 0).getText());
+ selectMenuPath("Component", "State", "Delayed edit of Row 0");
+ // Still the same data
+ assertEquals("Cell contents did not update correctly", "(0, 0)",
+ getGridElement().getCell(0, 0).getText());
+ sleep(5000);
+ // Data should be updated
+ assertEquals("Cell contents did not update correctly", "Bar",
+ getGridElement().getCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRefreshingWhenNotInViewThroughRowHandle() {
+ openTestURL();
+
+ assertEquals("Unexpected initial state", "(0, 0)", getGridElement()
+ .getCell(0, 0).getText());
+ getGridElement().scrollToRow(100);
+ selectMenuPath("Component", "State", "Edit and refresh Row 0");
+ assertEquals("Cell contents did not update correctly", "Foo",
+ getGridElement().getCell(0, 0).getText());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java
new file mode 100644
index 0000000000..cc801bf870
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+/**
+ * Abstract base class for header and footer tests.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class GridStaticSectionTest extends GridBasicClientFeaturesTest {
+
+ protected void assertHeaderTexts(int headerId, int rowIndex) {
+ int i = 0;
+ for (TestBenchElement cell : getGridElement().getHeaderCells(rowIndex)) {
+
+ if (i % 3 == 0) {
+ assertText(String.format("Header (%d,%d)", headerId, i), cell);
+ } else if (i % 2 == 0) {
+ assertHTML(String.format("<b>Header (%d,%d)</b>", headerId, i),
+ cell);
+ } else {
+ assertHTML(String.format(
+ "<div class=\"gwt-HTML\">Header (%d,%d)</div>",
+ headerId, i), cell);
+ }
+
+ i++;
+ }
+ assertEquals("number of header columns", GridBasicFeatures.COLUMNS, i);
+ }
+
+ protected void assertFooterTexts(int footerId, int rowIndex) {
+ int i = 0;
+ for (TestBenchElement cell : getGridElement().getFooterCells(rowIndex)) {
+ if (i % 3 == 0) {
+ assertText(String.format("Footer (%d,%d)", footerId, i), cell);
+ } else if (i % 2 == 0) {
+ assertHTML(String.format("<b>Footer (%d,%d)</b>", footerId, i),
+ cell);
+ } else {
+ assertHTML(String.format(
+ "<div class=\"gwt-HTML\">Footer (%d,%d)</div>",
+ footerId, i), cell);
+ }
+ i++;
+ }
+ assertEquals("number of footer columns", GridBasicFeatures.COLUMNS, i);
+ }
+
+ protected static void assertText(String text, TestBenchElement e) {
+ // TBE.getText returns "" if the element is scrolled out of view
+ assertEquals(text, e.getAttribute("innerHTML"));
+ }
+
+ protected static void assertHTML(String text, TestBenchElement e) {
+ String html = e.getAttribute("innerHTML");
+
+ // IE 8 returns tags as upper case while other browsers do not, make the
+ // comparison non-casesensive
+ html = html.toLowerCase();
+ text = text.toLowerCase();
+
+ // IE 8 returns attributes without quotes, make the comparison without
+ // quotes
+ html = html.replaceAll("\"", "");
+ text = html.replaceAll("\"", "");
+
+ assertEquals(text, html);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java
new file mode 100644
index 0000000000..cbf27a69d9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.By;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridStylingTest extends GridStaticSectionTest {
+
+ @Test
+ public void testGridPrimaryStyle() throws Exception {
+ openTestURL();
+
+ validateStylenames("v-grid");
+ }
+
+ @Test
+ public void testChangingPrimaryStyleName() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "State", "Primary Stylename",
+ "v-custom-style");
+
+ validateStylenames("v-custom-style");
+ }
+
+ private void validateStylenames(String stylename) {
+
+ String classNames = getGridElement().getAttribute("class");
+ assertEquals(stylename, classNames);
+
+ classNames = getGridElement().getVerticalScroller().getAttribute(
+ "class");
+ assertTrue(classNames.contains(stylename + "-scroller"));
+ assertTrue(classNames.contains(stylename + "-scroller-vertical"));
+
+ classNames = getGridElement().getHorizontalScroller().getAttribute(
+ "class");
+ assertTrue(classNames.contains(stylename + "-scroller"));
+ assertTrue(classNames.contains(stylename + "-scroller-horizontal"));
+
+ classNames = getGridElement().getTableWrapper().getAttribute("class");
+ assertEquals(stylename + "-tablewrapper", classNames);
+
+ classNames = getGridElement().getHeader().getAttribute("class");
+ assertEquals(stylename + "-header", classNames);
+
+ for (int row = 0; row < getGridElement().getHeaderCount(); row++) {
+ classNames = getGridElement().getHeaderRow(row).getAttribute(
+ "class");
+ assertEquals(stylename + "-row", classNames);
+
+ for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) {
+ classNames = getGridElement().getHeaderCell(row, col)
+ .getAttribute("class");
+ assertTrue(classNames.contains(stylename + "-cell"));
+ }
+ }
+
+ classNames = getGridElement().getBody().getAttribute("class");
+ assertEquals(stylename + "-body", classNames);
+
+ int rowsInBody = getGridElement().getBody()
+ .findElements(By.tagName("tr")).size();
+ for (int row = 0; row < rowsInBody; row++) {
+ classNames = getGridElement().getRow(row).getAttribute("class");
+ assertTrue(classNames.contains(stylename + "-row"));
+ assertTrue(classNames.contains(stylename + "-row-has-data"));
+
+ for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) {
+ classNames = getGridElement().getCell(row, col).getAttribute(
+ "class");
+ assertTrue(classNames.contains(stylename + "-cell"));
+
+ if (row == 0 && col == 0) {
+ assertTrue(classNames.contains(stylename + "-cell-focused"));
+ }
+ }
+ }
+
+ classNames = getGridElement().getFooter().getAttribute("class");
+ assertEquals(stylename + "-footer", classNames);
+
+ for (int row = 0; row < getGridElement().getFooterCount(); row++) {
+ classNames = getGridElement().getFooterRow(row).getAttribute(
+ "class");
+ assertEquals(stylename + "-row", classNames);
+
+ for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) {
+ classNames = getGridElement().getFooterCell(row, col)
+ .getAttribute("class");
+ assertTrue(classNames.contains(stylename + "-cell"));
+ }
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java
new file mode 100644
index 0000000000..95ed6ab3ff
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.escalator;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
+
+public class EscalatorBasicsTest extends EscalatorBasicClientFeaturesTest {
+
+ @Test
+ public void testDetachingAnEmptyEscalator() {
+ setDebug(true);
+ openTestURL();
+
+ selectMenuPath(GENERAL, DETACH_ESCALATOR);
+ assertEscalatorIsRemovedCorrectly();
+ }
+
+ @Test
+ public void testDetachingASemiPopulatedEscalator() throws IOException {
+ setDebug(true);
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, ADD_ONE_OF_EACH_ROW);
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ selectMenuPath(GENERAL, DETACH_ESCALATOR);
+ assertEscalatorIsRemovedCorrectly();
+ }
+
+ @Test
+ public void testDetachingAPopulatedEscalator() {
+ setDebug(true);
+ openTestURL();
+
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+ selectMenuPath(GENERAL, DETACH_ESCALATOR);
+ assertEscalatorIsRemovedCorrectly();
+ }
+
+ private void assertEscalatorIsRemovedCorrectly() {
+ assertFalse($(NotificationElement.class).exists());
+ assertNull(getEscalator());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColspanTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColspanTest.java
new file mode 100644
index 0000000000..d9b3debbe1
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColspanTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.escalator;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
+
+public class EscalatorColspanTest extends EscalatorBasicClientFeaturesTest {
+ private static final int NO_COLSPAN = 1;
+
+ @Test
+ public void testNoColspan() {
+ openTestURL();
+ populate();
+
+ assertEquals(NO_COLSPAN, getColSpan(getHeaderCell(0, 0)));
+ assertEquals(NO_COLSPAN, getColSpan(getBodyCell(0, 0)));
+ assertEquals(NO_COLSPAN, getColSpan(getFooterCell(0, 0)));
+ }
+
+ @Test
+ public void testColspan() {
+ openTestURL();
+ populate();
+
+ int firstCellWidth = getBodyCell(0, 0).getSize().getWidth();
+ int secondCellWidth = getBodyCell(0, 1).getSize().getWidth();
+ int doubleCellWidth = firstCellWidth + secondCellWidth;
+
+ selectMenuPath(FEATURES, COLUMN_SPANNING, COLSPAN_NORMAL);
+
+ WebElement bodyCell = getBodyCell(0, 0);
+ assertEquals("Cell was not spanned correctly", 2, getColSpan(bodyCell));
+ assertEquals(
+ "Spanned cell's width was not the sum of the previous cells ("
+ + firstCellWidth + " + " + secondCellWidth + ")",
+ doubleCellWidth, bodyCell.getSize().getWidth(), 1);
+ }
+
+ @Test
+ public void testColspanToggle() {
+ openTestURL();
+ populate();
+
+ int singleCellWidth = getBodyCell(0, 0).getSize().getWidth();
+
+ selectMenuPath(FEATURES, COLUMN_SPANNING, COLSPAN_NORMAL);
+ selectMenuPath(FEATURES, COLUMN_SPANNING, COLSPAN_NONE);
+
+ WebElement bodyCell = getBodyCell(0, 0);
+ assertEquals(NO_COLSPAN, getColSpan(bodyCell));
+ assertEquals(singleCellWidth, bodyCell.getSize().getWidth(), 1);
+ }
+
+ private static int getColSpan(WebElement cell) {
+ String attribute = cell.getAttribute("colspan");
+ if (attribute == null) {
+ return NO_COLSPAN;
+ } else {
+ return Integer.parseInt(attribute);
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColumnFreezingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColumnFreezingTest.java
new file mode 100644
index 0000000000..e808001cf7
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorColumnFreezingTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.escalator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
+
+public class EscalatorColumnFreezingTest extends
+ EscalatorBasicClientFeaturesTest {
+
+ private final static Pattern TRANSFORM_PATTERN = Pattern.compile(// @formatter:off
+ // any start of the string
+ ".*"
+
+ // non-capturing group for "webkitTransform: " or "transform: "
+ + "(?:webkitT|t)ransform: "
+
+ // non-capturing group for "translate" or "translate3d"
+ + "translate(?:3d)?"
+
+ // capturing the digits in e.g "(100px,"
+ + "\\((\\d+)px,"
+
+ // any end of the string
+ + ".*", Pattern.CASE_INSENSITIVE);
+
+ // @formatter:on
+
+ private final static Pattern LEFT_PATTERN = Pattern.compile(
+ ".*left: (\\d+)px.*", Pattern.CASE_INSENSITIVE);
+
+ private static final int NO_FREEZE = -1;
+
+ @Test
+ public void testNoFreeze() {
+ openTestURL();
+ populate();
+
+ WebElement bodyCell = getBodyCell(0, 0);
+ assertFalse(isFrozen(bodyCell));
+ assertEquals(NO_FREEZE, getFrozenScrollCompensation(bodyCell));
+ }
+
+ @Test
+ public void testOneFreeze() {
+ openTestURL();
+ populate();
+
+ selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN);
+ int scrollPx = 60;
+ scrollHorizontallyTo(scrollPx);
+
+ WebElement bodyCell = getBodyCell(0, 0);
+ assertTrue(isFrozen(bodyCell));
+ assertEquals(scrollPx, getFrozenScrollCompensation(bodyCell));
+ }
+
+ @Test
+ public void testFreezeToggle() {
+ openTestURL();
+ populate();
+
+ selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN);
+ scrollHorizontallyTo(100);
+ selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_0_COLUMNS);
+
+ WebElement bodyCell = getBodyCell(0, 0);
+ assertFalse(isFrozen(bodyCell));
+ assertEquals(NO_FREEZE, getFrozenScrollCompensation(bodyCell));
+ }
+
+ private static boolean isFrozen(WebElement cell) {
+ return cell.getAttribute("class").contains("frozen");
+ }
+
+ private static int getFrozenScrollCompensation(WebElement cell) {
+ String styleAttribute = cell.getAttribute("style");
+ Matcher transformMatcher = TRANSFORM_PATTERN.matcher(styleAttribute);
+ Matcher leftMatcher = LEFT_PATTERN.matcher(styleAttribute);
+
+ if (transformMatcher.find()) {
+ return Integer.parseInt(transformMatcher.group(1));
+ } else if (leftMatcher.find()) {
+ return Integer.parseInt(leftMatcher.group(1));
+ } else {
+ return NO_FREEZE;
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorRowColumnTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorRowColumnTest.java
new file mode 100644
index 0000000000..e72e8fae70
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorRowColumnTest.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.escalator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
+
+public class EscalatorRowColumnTest extends EscalatorBasicClientFeaturesTest {
+
+ /**
+ * The scroll position of the Escalator when scrolled all the way down, to
+ * reveal the 100:th row.
+ */
+ private static final int BOTTOM_SCROLL_POSITION = 1857;
+
+ @Test
+ public void testInit() {
+ openTestURL();
+ assertNotNull(getEscalator());
+ assertNull(getHeaderRow(0));
+ assertNull(getBodyRow(0));
+ assertNull(getFooterRow(0));
+
+ assertLogContains("Columns: 0");
+ assertLogContains("Header rows: 0");
+ assertLogContains("Body rows: 0");
+ assertLogContains("Footer rows: 0");
+ }
+
+ @Test
+ public void testInsertAColumn() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ assertNull(getHeaderRow(0));
+ assertNull(getBodyRow(0));
+ assertNull(getFooterRow(0));
+ assertLogContains("Columns: 1");
+ }
+
+ @Test
+ public void testInsertAHeaderRow() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, HEADER_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Header rows: 1");
+ }
+
+ @Test
+ public void testInsertABodyRow() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Body rows: 1");
+ }
+
+ @Test
+ public void testInsertAFooterRow() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, FOOTER_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Footer rows: 1");
+ }
+
+ @Test
+ public void testInsertAColumnAndAHeaderRow() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, HEADER_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ assertNotNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Columns: 1");
+ assertLogContains("Header rows: 1");
+ }
+
+ @Test
+ public void testInsertAColumnAndABodyRow() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNotNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Columns: 1");
+ assertLogContains("Body rows: 1");
+ }
+
+ @Test
+ public void testInsertAColumnAndAFooterRow() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, FOOTER_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNotNull(getFooterCell(0, 0));
+ assertLogContains("Columns: 1");
+ assertLogContains("Footer rows: 1");
+ }
+
+ @Test
+ public void testInsertAHeaderRowAndAColumn() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, HEADER_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ assertNotNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Columns: 1");
+ assertLogContains("Header rows: 1");
+ }
+
+ @Test
+ public void testInsertABodyRowAndAColumn() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNotNull(getBodyCell(0, 0));
+ assertNull(getFooterCell(0, 0));
+ assertLogContains("Columns: 1");
+ assertLogContains("Body rows: 1");
+ }
+
+ @Test
+ public void testInsertAFooterRowAndAColumn() {
+ openTestURL();
+
+ selectMenuPath(COLUMNS_AND_ROWS, FOOTER_ROWS, ADD_ONE_ROW_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, ADD_ONE_COLUMN_TO_BEGINNING);
+ assertNull(getHeaderCell(0, 0));
+ assertNull(getBodyCell(0, 0));
+ assertNotNull(getFooterCell(0, 0));
+ assertLogContains("Columns: 1");
+ assertLogContains("Footer rows: 1");
+ }
+
+ @Test
+ public void testFillColRow() {
+ openTestURL();
+
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+ scrollVerticallyTo(2000); // more like 1857, but this should be enough.
+
+ // if not found, an exception is thrown here
+ assertTrue("Wanted cell was not visible",
+ isElementPresent(By.xpath("//td[text()='Cell: 9,99']")));
+ }
+
+ @Test
+ public void testFillRowCol() {
+ openTestURL();
+
+ selectMenuPath(GENERAL, POPULATE_ROW_COLUMN);
+ scrollVerticallyTo(2000); // more like 1857, but this should be enough.
+
+ // if not found, an exception is thrown here
+ assertTrue("Wanted cell was not visible",
+ isElementPresent(By.xpath("//td[text()='Cell: 9,99']")));
+ }
+
+ @Test
+ public void testClearColRow() {
+ openTestURL();
+
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+ selectMenuPath(GENERAL, CLEAR_COLUMN_ROW);
+
+ assertNull(getBodyCell(0, 0));
+ }
+
+ @Test
+ public void testClearRowCol() {
+ openTestURL();
+
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+ selectMenuPath(GENERAL, CLEAR_ROW_COLUMN);
+
+ assertNull(getBodyCell(0, 0));
+ }
+
+ @Test
+ public void testResizeColToFit() {
+ openTestURL();
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS, RESIZE_FIRST_COLUMN_TO_100PX);
+ int originalWidth = getBodyCell(0, 0).getSize().getWidth();
+
+ assertEquals(100, originalWidth);
+
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS,
+ RESIZE_FIRST_COLUMN_TO_MAX_WIDTH);
+ int newWidth = getBodyCell(0, 0).getSize().getWidth();
+ assertNotEquals("Column width should've changed", originalWidth,
+ newWidth);
+ }
+
+ @Test
+ public void testRemoveMoreThanPagefulAtBottomWhileScrolledToBottom()
+ throws Exception {
+ openTestURL();
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+
+ scrollVerticallyTo(BOTTOM_SCROLL_POSITION);
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_50_ROWS_FROM_BOTTOM);
+ assertEquals("Row 49: 0,49", getBodyCell(-1, 0).getText());
+
+ scrollVerticallyTo(0);
+
+ // let the DOM organize itself
+ Thread.sleep(500);
+
+ // if something goes wrong, it'll explode before this.
+ assertEquals("Row 0: 0,0", getBodyCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRemoveMoreThanPagefulAtBottomWhileScrolledAlmostToBottom()
+ throws Exception {
+ openTestURL();
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+
+ // bottom minus 15 rows.
+ scrollVerticallyTo(BOTTOM_SCROLL_POSITION - 15 * 20);
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_50_ROWS_FROM_BOTTOM);
+ assertEquals("Row 49: 0,49", getBodyCell(-1, 0).getText());
+
+ scrollVerticallyTo(0);
+
+ // let the DOM organize itself
+ Thread.sleep(500);
+
+ // if something goes wrong, it'll explode before this.
+ assertEquals("Row 0: 0,0", getBodyCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRemoveMoreThanPagefulNearBottomWhileScrolledToBottom()
+ throws Exception {
+ openTestURL();
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+
+ scrollVerticallyTo(BOTTOM_SCROLL_POSITION);
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
+ REMOVE_50_ROWS_FROM_ALMOST_BOTTOM);
+ assertEquals("Row 49: 0,99", getBodyCell(-1, 0).getText());
+
+ scrollVerticallyTo(0);
+
+ // let the DOM organize itself
+ Thread.sleep(500);
+
+ // if something goes wrong, it'll explode before this.
+ assertEquals("Row 0: 0,0", getBodyCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRemoveMoreThanPagefulNearBottomWhileScrolledAlmostToBottom()
+ throws Exception {
+ openTestURL();
+ selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
+
+ // bottom minus 15 rows.
+ scrollVerticallyTo(BOTTOM_SCROLL_POSITION - 15 * 20);
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
+ REMOVE_50_ROWS_FROM_ALMOST_BOTTOM);
+
+ // let the DOM organize itself
+ Thread.sleep(500);
+ assertEquals("Row 49: 0,99", getBodyCell(-1, 0).getText());
+
+ scrollVerticallyTo(0);
+
+ // let the DOM organize itself
+ Thread.sleep(500);
+
+ // if something goes wrong, it'll explode before this.
+ assertEquals("Row 0: 0,0", getBodyCell(0, 0).getText());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorScrollTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorScrollTest.java
new file mode 100644
index 0000000000..41ad370b98
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorScrollTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.escalator;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
+
+@SuppressWarnings("all")
+public class EscalatorScrollTest extends EscalatorBasicClientFeaturesTest {
+
+ @Before
+ public void setUp() {
+ openTestURL();
+ populate();
+ }
+
+ /**
+ * Before the fix, removing and adding rows and also scrolling would put the
+ * scroll state in an internally inconsistent state. The scrollbar would've
+ * been scrolled correctly, but the body wasn't.
+ *
+ * This was due to optimizations that didn't keep up with the promises, so
+ * to say. So the optimizations were removed.
+ */
+ @Test
+ public void testScrollRaceCondition() {
+ scrollVerticallyTo(40);
+ String originalStyle = getTBodyStyle();
+ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_INSERT_SCROLL);
+
+ // body should be scrolled to exactly the same spot. (not 0)
+ assertEquals(originalStyle, getTBodyStyle());
+ }
+
+ @Test
+ public void scrollToBottomAndRemoveHeader() throws Exception {
+ scrollVerticallyTo(999999); // to bottom
+
+ /*
+ * apparently the scroll event isn't fired by the time the next assert
+ * would've been done.
+ */
+ Thread.sleep(50);
+
+ assertEquals("Unexpected last row cell before header removal",
+ "Row 99: 0,99", getBodyCell(-1, 0).getText());
+ selectMenuPath(COLUMNS_AND_ROWS, HEADER_ROWS,
+ REMOVE_ONE_ROW_FROM_BEGINNING);
+ assertEquals("Unexpected last row cell after header removal",
+ "Row 99: 0,99", getBodyCell(-1, 0).getText());
+
+ }
+
+ @Test
+ public void scrollToBottomAndRemoveFooter() throws Exception {
+ scrollVerticallyTo(999999); // to bottom
+
+ /*
+ * apparently the scroll event isn't fired by the time the next assert
+ * would've been done.
+ */
+ Thread.sleep(50);
+
+ assertEquals("Unexpected last row cell before footer removal",
+ "Row 99: 0,99", getBodyCell(-1, 0).getText());
+ selectMenuPath(COLUMNS_AND_ROWS, FOOTER_ROWS,
+ REMOVE_ONE_ROW_FROM_BEGINNING);
+ assertEquals("Unexpected last row cell after footer removal",
+ "Row 99: 0,99", getBodyCell(-1, 0).getText());
+ }
+
+ private String getTBodyStyle() {
+ WebElement tbody = getEscalator().findElement(By.tagName("tbody"));
+ return tbody.getAttribute("style");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorUpdaterUiTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorUpdaterUiTest.java
new file mode 100644
index 0000000000..85d3fc0bac
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorUpdaterUiTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.escalator;
+
+import org.junit.Test;
+
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
+import com.vaadin.tests.components.grid.basicfeatures.EscalatorUpdaterUi;
+
+public class EscalatorUpdaterUiTest extends EscalatorBasicClientFeaturesTest {
+ @Override
+ protected Class<?> getUIClass() {
+ return EscalatorUpdaterUi.class;
+ }
+
+ @Test
+ public void testHeaderPaintOrderRowColRowCol() {
+ boolean addColumnFirst = false;
+ boolean removeColumnFirst = false;
+ testPaintOrder(HEADER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testHeaderPaintOrderRowColColRow() {
+ boolean addColumnFirst = false;
+ boolean removeColumnFirst = true;
+ testPaintOrder(HEADER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testHeaderPaintOrderColRowColRow() {
+ boolean addColumnFirst = true;
+ boolean removeColumnFirst = true;
+ testPaintOrder(HEADER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testHeaderPaintOrderColRowRowCol() {
+ boolean addColumnFirst = true;
+ boolean removeColumnFirst = false;
+ testPaintOrder(HEADER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testBodyPaintOrderRowColRowCol() {
+ boolean addColumnFirst = false;
+ boolean removeColumnFirst = false;
+ testPaintOrder(BODY_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testBodyPaintOrderRowColColRow() {
+ boolean addColumnFirst = false;
+ boolean removeColumnFirst = true;
+ testPaintOrder(BODY_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testBodyPaintOrderColRowColRow() {
+ boolean addColumnFirst = true;
+ boolean removeColumnFirst = true;
+ testPaintOrder(BODY_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testBodyPaintOrderColRowRowCol() {
+ boolean addColumnFirst = true;
+ boolean removeColumnFirst = false;
+ testPaintOrder(BODY_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testFooterPaintOrderRowColRowCol() {
+ boolean addColumnFirst = false;
+ boolean removeColumnFirst = false;
+ testPaintOrder(FOOTER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testFooterPaintOrderRowColColRow() {
+ boolean addColumnFirst = false;
+ boolean removeColumnFirst = true;
+ testPaintOrder(FOOTER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testFooterPaintOrderColRowColRow() {
+ boolean addColumnFirst = true;
+ boolean removeColumnFirst = true;
+ testPaintOrder(FOOTER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ @Test
+ public void testFooterPaintOrderColRowRowCol() {
+ boolean addColumnFirst = true;
+ boolean removeColumnFirst = false;
+ testPaintOrder(FOOTER_ROWS, addColumnFirst, removeColumnFirst);
+ }
+
+ private void testPaintOrder(String tableSection, boolean addColumnFirst,
+ boolean removeColumnFirst) {
+ openTestURL();
+
+ if (addColumnFirst) {
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS,
+ ADD_ONE_COLUMN_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, tableSection,
+ ADD_ONE_ROW_TO_BEGINNING);
+ } else {
+ selectMenuPath(COLUMNS_AND_ROWS, tableSection,
+ ADD_ONE_ROW_TO_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS,
+ ADD_ONE_COLUMN_TO_BEGINNING);
+ }
+
+ assertLogContainsInOrder("preAttach: elementIsAttached == false",
+ "postAttach: elementIsAttached == true",
+ "update: elementIsAttached == true");
+ assertLogDoesNotContain("preDetach");
+ assertLogDoesNotContain("postDetach");
+
+ if (removeColumnFirst) {
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS,
+ REMOVE_ONE_COLUMN_FROM_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, tableSection,
+ REMOVE_ONE_ROW_FROM_BEGINNING);
+ } else {
+ selectMenuPath(COLUMNS_AND_ROWS, tableSection,
+ REMOVE_ONE_ROW_FROM_BEGINNING);
+ selectMenuPath(COLUMNS_AND_ROWS, COLUMNS,
+ REMOVE_ONE_COLUMN_FROM_BEGINNING);
+ }
+
+ assertLogContainsInOrder("preDetach: elementIsAttached == true",
+ "postDetach: elementIsAttached == false");
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/DisabledGridTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/DisabledGridTest.java
new file mode 100644
index 0000000000..ed1234e608
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/DisabledGridTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement.GridRowElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class DisabledGridTest extends GridBasicFeaturesTest {
+
+ @Before
+ public void setUp() {
+ openTestURL();
+ selectMenuPath("Component", "State", "Enabled");
+ }
+
+ @Test
+ public void testSelection() {
+ selectMenuPath("Component", "State", "Selection mode", "single");
+
+ GridRowElement row = getGridElement().getRow(0);
+ row.click();
+ assertFalse("disabled row should not be selected", row.isSelected());
+
+ }
+
+ @Test
+ public void testEditorOpening() {
+ selectMenuPath("Component", "Editor", "Enabled");
+
+ GridRowElement row = getGridElement().getRow(0);
+ row.click();
+ assertNull("Editor should not open", getEditor());
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+ assertNull("Editor should not open", getEditor());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellFocusAdjustmentTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellFocusAdjustmentTest.java
new file mode 100644
index 0000000000..0c26ceb5c9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellFocusAdjustmentTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridCellFocusAdjustmentTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testCellFocusWithAddAndRemoveRows() {
+ openTestURL();
+ GridElement grid = getGridElement();
+
+ grid.getCell(0, 0).click();
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertTrue("Cell focus was not moved when adding a row",
+ grid.getCell(1, 0).isFocused());
+
+ selectMenuPath("Component", "Body rows", "Add 18 rows");
+ assertTrue("Cell focus was not moved when adding multiple rows", grid
+ .getCell(19, 0).isFocused());
+
+ for (int i = 18; i <= 0; --i) {
+ selectMenuPath("Component", "Body rows", "Remove first row");
+ assertTrue("Cell focus was not moved when removing a row", grid
+ .getCell(i, 0).isFocused());
+ }
+ }
+
+ @Test
+ public void testCellFocusOffsetWhileInDifferentSection() {
+ openTestURL();
+ getGridElement().getCell(0, 0).click();
+ new Actions(getDriver()).sendKeys(Keys.UP).perform();
+ assertTrue("Header 0,0 should've become focused", getGridElement()
+ .getHeaderCell(0, 0).isFocused());
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertTrue("Header 0,0 should've remained focused", getGridElement()
+ .getHeaderCell(0, 0).isFocused());
+ }
+
+ @Test
+ public void testCellFocusOffsetWhileInSameSectionAndInsertedAbove() {
+ openTestURL();
+ assertTrue("Body 0,0 should've gotten focus",
+ getGridElement().getCell(0, 0).isFocused());
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertTrue("Body 1,0 should've gotten focus",
+ getGridElement().getCell(1, 0).isFocused());
+ }
+
+ @Test
+ public void testCellFocusOffsetWhileInSameSectionAndInsertedBelow() {
+ openTestURL();
+ assertTrue("Body 0,0 should've gotten focus",
+ getGridElement().getCell(0, 0).isFocused());
+
+ selectMenuPath("Component", "Body rows", "Add third row");
+ assertTrue("Body 0,0 should've remained focused", getGridElement()
+ .getCell(0, 0).isFocused());
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellStyleGeneratorTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellStyleGeneratorTest.java
new file mode 100644
index 0000000000..643c61d90a
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridCellStyleGeneratorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.GridElement.GridRowElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridCellStyleGeneratorTest extends GridBasicFeaturesTest {
+ @Test
+ public void testStyleNameGeneratorScrolling() throws Exception {
+ openTestURL();
+
+ selectRowStyleNameGenerator(GridBasicFeatures.ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4);
+ selectCellStyleNameGenerator(GridBasicFeatures.CELL_STYLE_GENERATOR_SPECIAL);
+
+ GridRowElement row = getGridElement().getRow(2);
+ GridCellElement cell = getGridElement().getCell(3, 2);
+
+ Assert.assertTrue(hasCssClass(row, "row2"));
+ Assert.assertTrue(hasCssClass(cell, "Column_2"));
+
+ // Scroll down and verify that the old elements don't have the
+ // stylename any more
+
+ // Carefully chosen offset to hit an index % 4 without cell style
+ row = getGridElement().getRow(352);
+ cell = getGridElement().getCell(353, 2);
+
+ Assert.assertFalse(hasCssClass(row, "row352"));
+ Assert.assertFalse(hasCssClass(cell, "Column_2"));
+ }
+
+ @Test
+ public void testDisableStyleNameGenerator() throws Exception {
+ openTestURL();
+
+ selectRowStyleNameGenerator(GridBasicFeatures.ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4);
+ selectCellStyleNameGenerator(GridBasicFeatures.CELL_STYLE_GENERATOR_SPECIAL);
+
+ // Just verify that change was effective
+ GridRowElement row2 = getGridElement().getRow(2);
+ GridCellElement cell3_2 = getGridElement().getCell(3, 2);
+
+ Assert.assertTrue(hasCssClass(row2, "row2"));
+ Assert.assertTrue(hasCssClass(cell3_2, "Column_2"));
+
+ // Disable the generator and check again
+ selectRowStyleNameGenerator(GridBasicFeatures.ROW_STYLE_GENERATOR_NONE);
+ selectCellStyleNameGenerator(GridBasicFeatures.CELL_STYLE_GENERATOR_NONE);
+
+ Assert.assertFalse(hasCssClass(row2, "row2"));
+ Assert.assertFalse(hasCssClass(cell3_2, "Column_2"));
+ }
+
+ @Test
+ public void testChangeStyleNameGenerator() throws Exception {
+ openTestURL();
+
+ selectRowStyleNameGenerator(GridBasicFeatures.ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4);
+ selectCellStyleNameGenerator(GridBasicFeatures.CELL_STYLE_GENERATOR_SPECIAL);
+
+ // Just verify that change was effective
+ GridRowElement row2 = getGridElement().getRow(2);
+ GridCellElement cell3_2 = getGridElement().getCell(3, 2);
+
+ Assert.assertTrue(hasCssClass(row2, "row2"));
+ Assert.assertTrue(hasCssClass(cell3_2, "Column_2"));
+
+ // Change the generator and check again
+ selectRowStyleNameGenerator(GridBasicFeatures.ROW_STYLE_GENERATOR_NONE);
+ selectCellStyleNameGenerator(GridBasicFeatures.CELL_STYLE_GENERATOR_PROPERTY_TO_STRING);
+
+ // Old styles removed?
+ Assert.assertFalse(hasCssClass(row2, "row2"));
+ Assert.assertFalse(hasCssClass(cell3_2, "Column_2"));
+
+ // New style present?
+ Assert.assertTrue(hasCssClass(cell3_2, "Column-2"));
+ }
+
+ @Test
+ public void testCellStyleGeneratorWithSelectionColumn() {
+ setDebug(true);
+ openTestURL();
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+
+ selectCellStyleNameGenerator(GridBasicFeatures.CELL_STYLE_GENERATOR_SPECIAL);
+
+ assertFalse("Error notification was present",
+ isElementPresent(NotificationElement.class));
+ }
+
+ private void selectRowStyleNameGenerator(String name) {
+ selectMenuPath("Component", "State", "Row style generator", name);
+ }
+
+ private void selectCellStyleNameGenerator(String name) {
+ selectMenuPath("Component", "State", "Cell style generator", name);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridEditorTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridEditorTest.java
new file mode 100644
index 0000000000..0218fffe61
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridEditorTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.GridElement.GridEditorElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridEditorTest extends GridBasicFeaturesTest {
+
+ private static final String[] EDIT_ITEM_5 = new String[] { "Component",
+ "Editor", "Edit item 5" };
+ private static final String[] EDIT_ITEM_100 = new String[] { "Component",
+ "Editor", "Edit item 100" };
+ private static final String[] TOGGLE_EDIT_ENABLED = new String[] {
+ "Component", "Editor", "Enabled" };
+
+ @Before
+ public void setUp() {
+ setDebug(true);
+ openTestURL();
+ selectMenuPath(TOGGLE_EDIT_ENABLED);
+ }
+
+ @Test
+ public void testProgrammaticOpeningClosing() {
+ selectMenuPath(EDIT_ITEM_5);
+ assertEditorOpen();
+
+ selectMenuPath("Component", "Editor", "Cancel edit");
+ assertEditorClosed();
+ }
+
+ @Test
+ public void testProgrammaticOpeningWhenDisabled() {
+ selectMenuPath(TOGGLE_EDIT_ENABLED);
+ selectMenuPath(EDIT_ITEM_5);
+ assertEditorClosed();
+ boolean thrown = logContainsText("Exception occured, java.lang.IllegalStateException");
+ assertTrue("IllegalStateException thrown", thrown);
+ }
+
+ @Test
+ public void testDisablingWhileOpen() {
+ selectMenuPath(EDIT_ITEM_5);
+ selectMenuPath(TOGGLE_EDIT_ENABLED);
+ assertEditorOpen();
+ boolean thrown = logContainsText("Exception occured, java.lang.IllegalStateException");
+ assertTrue("IllegalStateException thrown", thrown);
+ }
+
+ @Test
+ public void testProgrammaticOpeningWithScroll() {
+ selectMenuPath(EDIT_ITEM_100);
+ assertEditorOpen();
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void testVerticalScrollLocking() {
+ selectMenuPath(EDIT_ITEM_5);
+ getGridElement().getCell(200, 0);
+ }
+
+ @Test
+ public void testKeyboardOpeningClosing() {
+
+ getGridElement().getCell(4, 0).click();
+ assertEditorClosed();
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+ assertEditorOpen();
+
+ new Actions(getDriver()).sendKeys(Keys.ESCAPE).perform();
+ assertEditorClosed();
+
+ // Disable Editor
+ selectMenuPath(TOGGLE_EDIT_ENABLED);
+ getGridElement().getCell(5, 0).click();
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+ assertEditorClosed();
+ }
+
+ @Test
+ public void testComponentBinding() {
+ selectMenuPath(EDIT_ITEM_100);
+
+ List<WebElement> widgets = getEditorWidgets();
+ assertEquals("Number of widgets", GridBasicFeatures.COLUMNS,
+ widgets.size());
+
+ assertEquals("(100, 0)", widgets.get(0).getAttribute("value"));
+ assertEquals("(100, 1)", widgets.get(1).getAttribute("value"));
+ assertEquals("(100, 2)", widgets.get(2).getAttribute("value"));
+ assertEquals("<b>100</b>", widgets.get(9).getAttribute("value"));
+ }
+
+ @Test
+ public void testSave() {
+ selectMenuPath(EDIT_ITEM_100);
+
+ WebElement textField = getEditorWidgets().get(0);
+
+ textField.click();
+
+ textField.sendKeys(" changed");
+
+ WebElement saveButton = getEditor().findElement(
+ By.className("v-grid-editor-save"));
+
+ saveButton.click();
+
+ assertEquals("(100, 0) changed", getGridElement().getCell(100, 0)
+ .getText());
+ }
+
+ @Test
+ public void testProgrammaticSave() {
+ selectMenuPath(EDIT_ITEM_100);
+
+ WebElement textField = getEditorWidgets().get(0);
+
+ textField.click();
+
+ textField.sendKeys(" changed");
+
+ selectMenuPath("Component", "Editor", "Save");
+
+ assertEquals("(100, 0) changed", getGridElement().getCell(100, 0)
+ .getText());
+ }
+
+ private void assertEditorOpen() {
+ assertNotNull("Editor is supposed to be open", getEditor());
+ assertEquals("Unexpected number of widgets", GridBasicFeatures.COLUMNS,
+ getEditorWidgets().size());
+ }
+
+ private void assertEditorClosed() {
+ assertNull("Editor is supposed to be closed", getEditor());
+ }
+
+ private List<WebElement> getEditorWidgets() {
+ assertNotNull(getEditor());
+ return getEditor().findElements(By.className("v-textfield"));
+
+ }
+
+ @Test
+ public void testInvalidEdition() {
+ selectMenuPath(EDIT_ITEM_5);
+ assertFalse(logContainsText("Exception occured, java.lang.IllegalStateException"));
+
+ GridEditorElement editor = getGridElement().getEditor();
+
+ WebElement intField = editor.getField(7);
+ intField.clear();
+ intField.sendKeys("banana phone");
+ editor.save();
+ assertTrue(
+ "No exception on invalid value.",
+ logContainsText("Exception occured, com.vaadin.data.fieldgroup.FieldGroup$CommitException: Commit failed"));
+ editor.cancel();
+
+ selectMenuPath(EDIT_ITEM_100);
+ assertFalse("Exception should not exist",
+ isElementPresent(NotificationElement.class));
+ }
+
+ @Test
+ public void testNoScrollAfterEditByAPI() {
+ int originalScrollPos = getGridVerticalScrollPos();
+
+ selectMenuPath(EDIT_ITEM_5);
+
+ scrollGridVerticallyTo(100);
+ assertEquals("Grid shouldn't scroll vertically while editing",
+ originalScrollPos, getGridVerticalScrollPos());
+ }
+
+ @Test
+ public void testNoScrollAfterEditByMouse() {
+ int originalScrollPos = getGridVerticalScrollPos();
+
+ GridCellElement cell_5_0 = getGridElement().getCell(5, 0);
+ new Actions(getDriver()).doubleClick(cell_5_0).perform();
+
+ scrollGridVerticallyTo(100);
+ assertEquals("Grid shouldn't scroll vertically while editing",
+ originalScrollPos, getGridVerticalScrollPos());
+ }
+
+ @Test
+ public void testNoScrollAfterEditByKeyboard() {
+ int originalScrollPos = getGridVerticalScrollPos();
+
+ GridCellElement cell_5_0 = getGridElement().getCell(5, 0);
+ cell_5_0.click();
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ scrollGridVerticallyTo(100);
+ assertEquals("Grid shouldn't scroll vertically while editing",
+ originalScrollPos, getGridVerticalScrollPos());
+ }
+
+ @Test
+ public void testEditorInDisabledGrid() {
+ int originalScrollPos = getGridVerticalScrollPos();
+
+ selectMenuPath(EDIT_ITEM_5);
+ assertEditorOpen();
+
+ selectMenuPath("Component", "State", "Enabled");
+ assertEditorOpen();
+
+ GridEditorElement editor = getGridElement().getEditor();
+ editor.save();
+ assertEditorOpen();
+
+ editor.cancel();
+ assertEditorOpen();
+
+ selectMenuPath("Component", "State", "Enabled");
+
+ scrollGridVerticallyTo(100);
+ assertEquals("Grid shouldn't scroll vertically while editing",
+ originalScrollPos, getGridVerticalScrollPos());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridItemClickTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridItemClickTest.java
new file mode 100644
index 0000000000..57fc56c995
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridItemClickTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridItemClickTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testItemClick() {
+ openTestURL();
+
+ selectMenuPath("Component", "State", "ItemClickListener");
+
+ GridCellElement cell = getGridElement().getCell(3, 2);
+ new Actions(getDriver()).moveToElement(cell).click().perform();
+
+ assertTrue("No click in log", logContainsText(itemClickOn(3, 2, false)));
+ }
+
+ @Test
+ public void testItemDoubleClick() {
+ openTestURL();
+
+ selectMenuPath("Component", "State", "ItemClickListener");
+
+ GridCellElement cell = getGridElement().getCell(3, 2);
+ new Actions(getDriver()).moveToElement(cell).doubleClick().perform();
+
+ assertTrue("No double click in log",
+ logContainsText(itemClickOn(3, 2, true)));
+ }
+
+ private String itemClickOn(int row, int column, boolean dblClick) {
+ return "Item " + (dblClick ? "double " : "") + "click on Column "
+ + column + ", item " + row;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java
new file mode 100644
index 0000000000..3f2e82793b
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridKeyboardNavigationTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testCellFocusOnClick() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ assertTrue("Body cell 0, 0 is not focused on init.", grid.getCell(0, 0)
+ .isFocused());
+ grid.getCell(5, 2).click();
+ assertFalse("Body cell 0, 0 was still focused after clicking", grid
+ .getCell(0, 0).isFocused());
+ assertTrue("Body cell 5, 2 is not focused after clicking", grid
+ .getCell(5, 2).isFocused());
+ }
+
+ @Test
+ public void testCellNotFocusedWhenRendererHandlesEvent() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ assertTrue("Body cell 0, 0 is not focused on init.", grid.getCell(0, 0)
+ .isFocused());
+ grid.getHeaderCell(0, 3).click();
+ assertFalse("Body cell 0, 0 is focused after click on header.", grid
+ .getCell(0, 0).isFocused());
+ assertTrue("Header cell 0, 3 is not focused after click on header.",
+ grid.getHeaderCell(0, 3).isFocused());
+ }
+
+ @Test
+ public void testSimpleKeyboardNavigation() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ grid.getCell(0, 0).click();
+
+ new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform();
+ assertTrue("Body cell 1, 0 is not focused after keyboard navigation.",
+ grid.getCell(1, 0).isFocused());
+
+ new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform();
+ assertTrue("Body cell 1, 1 is not focused after keyboard navigation.",
+ grid.getCell(1, 1).isFocused());
+
+ int i;
+ for (i = 1; i < 40; ++i) {
+ new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform();
+ }
+
+ assertFalse("Grid has not scrolled with cell focus",
+ isElementPresent(By.xpath("//td[text() = '(0, 0)']")));
+ assertTrue("Cell focus is not visible",
+ isElementPresent(By.xpath("//td[text() = '(" + i + ", 0)']")));
+ assertTrue("Body cell " + i + ", 1 is not focused", grid.getCell(i, 1)
+ .isFocused());
+ }
+
+ @Test
+ public void testNavigateFromHeaderToBody() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ grid.scrollToRow(300);
+ new Actions(driver).moveToElement(grid.getHeaderCell(0, 7)).click()
+ .perform();
+ grid.scrollToRow(280);
+
+ assertTrue("Header cell is not focused.", grid.getHeaderCell(0, 7)
+ .isFocused());
+ new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform();
+ assertTrue("Body cell 280, 7 is not focused", grid.getCell(280, 7)
+ .isFocused());
+ }
+
+ @Test
+ public void testNavigationFromFooterToBody() {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ GridElement grid = getGridElement();
+ grid.scrollToRow(300);
+ grid.getFooterCell(0, 2).click();
+
+ assertTrue("Footer cell does not have focus.", grid.getFooterCell(0, 2)
+ .isFocused());
+ new Actions(getDriver()).sendKeys(Keys.ARROW_UP).perform();
+ assertTrue("Body cell 300, 2 does not have focus.", grid
+ .getCell(300, 2).isFocused());
+ }
+
+ @Test
+ public void testNavigateBetweenHeaderAndBodyWithTab() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ grid.getCell(10, 2).click();
+
+ assertTrue("Body cell 10, 2 does not have focus", grid.getCell(10, 2)
+ .isFocused());
+ new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB)
+ .keyUp(Keys.SHIFT).perform();
+ assertTrue("Header cell 0, 2 does not have focus",
+ grid.getHeaderCell(0, 2).isFocused());
+ new Actions(getDriver()).sendKeys(Keys.TAB).perform();
+ assertTrue("Body cell 10, 2 does not have focus", grid.getCell(10, 2)
+ .isFocused());
+
+ // Navigate out of the Grid and try to navigate with arrow keys.
+ new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB)
+ .sendKeys(Keys.TAB).keyUp(Keys.SHIFT).sendKeys(Keys.ARROW_DOWN)
+ .perform();
+ assertTrue("Header cell 0, 2 does not have focus",
+ grid.getHeaderCell(0, 2).isFocused());
+ }
+
+ @Test
+ public void testNavigateBetweenFooterAndBodyWithTab() {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ GridElement grid = getGridElement();
+ grid.getCell(10, 2).click();
+
+ assertTrue("Body cell 10, 2 does not have focus", grid.getCell(10, 2)
+ .isFocused());
+ new Actions(getDriver()).sendKeys(Keys.TAB).perform();
+ assertTrue("Footer cell 0, 2 does not have focus",
+ grid.getFooterCell(0, 2).isFocused());
+ new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB)
+ .keyUp(Keys.SHIFT).perform();
+ assertTrue("Body cell 10, 2 does not have focus", grid.getCell(10, 2)
+ .isFocused());
+
+ // Navigate out of the Grid and try to navigate with arrow keys.
+ new Actions(getDriver()).sendKeys(Keys.TAB).sendKeys(Keys.TAB)
+ .sendKeys(Keys.ARROW_UP).perform();
+ assertTrue("Footer cell 0, 2 does not have focus",
+ grid.getFooterCell(0, 2).isFocused());
+ }
+
+ @Test
+ public void testHomeEnd() throws Exception {
+ openTestURL();
+
+ getGridElement().getCell(100, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.HOME).perform();
+ assertTrue("First row is not visible", getGridElement().getCell(0, 2)
+ .isDisplayed());
+
+ new Actions(getDriver()).sendKeys(Keys.END).perform();
+ assertTrue("Last row cell not visible",
+ getGridElement().getCell(GridBasicFeatures.ROWS - 1, 2)
+ .isDisplayed());
+ }
+
+ @Test
+ public void testPageUpPageDown() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Size", "HeightMode Row");
+
+ getGridElement().getCell(5, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_DOWN).perform();
+ assertTrue("Row 20 did not become visible",
+ isElementPresent(By.xpath("//td[text() = '(20, 2)']")));
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_DOWN).perform();
+ assertTrue("Row 30 did not become visible",
+ isElementPresent(By.xpath("//td[text() = '(30, 2)']")));
+
+ assertTrue("Originally focused cell is no longer focused",
+ getGridElement().getCell(5, 2).isFocused());
+
+ getGridElement().getCell(50, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_UP).perform();
+ assertTrue("Row 31 did not become visible",
+ isElementPresent(By.xpath("//td[text() = '(31, 2)']")));
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_UP).perform();
+ assertTrue("Row 21 did not become visible",
+ isElementPresent(By.xpath("//td[text() = '(21, 2)']")));
+
+ assertTrue("Originally focused cell is no longer focused",
+ getGridElement().getCell(50, 2).isFocused());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridMultiSortingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridMultiSortingTest.java
new file mode 100644
index 0000000000..a61ed33029
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridMultiSortingTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridMultiSortingTest extends GridBasicFeaturesTest {
+
+ @Override
+ public List<DesiredCapabilities> getBrowsersToTest() {
+ List<DesiredCapabilities> browsersToTest = super.getBrowsersToTest();
+ /* FireFox and PhantomJS don't know how to press Shift key... */
+ browsersToTest.remove(Browser.FIREFOX.getDesiredCapabilities());
+ browsersToTest.remove(Browser.PHANTOMJS.getDesiredCapabilities());
+ return browsersToTest;
+ }
+
+ @Test
+ public void testUserMultiColumnSorting() {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 11", "Column 11 Width",
+ "Auto");
+
+ GridCellElement cell = getGridElement().getHeaderCell(0, 11);
+ new Actions(driver).moveToElement(cell, 5, 5).click().perform();
+ new Actions(driver).keyDown(Keys.SHIFT).perform();
+ new Actions(driver)
+ .moveToElement(getGridElement().getHeaderCell(0, 0), 5, 5)
+ .click().perform();
+ new Actions(driver).keyUp(Keys.SHIFT).perform();
+
+ String prev = getGridElement().getCell(0, 11).getAttribute("innerHTML");
+ for (int i = 1; i <= 6; ++i) {
+ assertEquals("Column 11 should contain same values.", prev,
+ getGridElement().getCell(i, 11).getAttribute("innerHTML"));
+ }
+
+ prev = getGridElement().getCell(0, 0).getText();
+ for (int i = 1; i <= 6; ++i) {
+ assertTrue(
+ "Grid is not sorted by column 0.",
+ prev.compareTo(getGridElement().getCell(i, 0).getText()) < 0);
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridRowAddRemoveTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridRowAddRemoveTest.java
new file mode 100644
index 0000000000..8535efb9ef
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridRowAddRemoveTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridRowAddRemoveTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void addRows_loadAllAtOnce() {
+ openTestURL();
+
+ selectMenuPath("Settings", "Clear log");
+ selectMenuPath("Component", "Body rows", "Remove all rows");
+ selectMenuPath("Component", "Body rows", "Add 18 rows");
+
+ Assert.assertTrue(
+ "All added rows should be fetched in the same round trip.",
+ logContainsText("Requested items 0 - 18"));
+ }
+
+ @Test
+ public void removeRows_loadAllAtOnce() {
+ openTestURL();
+
+ selectMenuPath("Component", "Size", "HeightMode Row");
+ selectMenuPath("Settings", "Clear log");
+ selectMenuPath("Component", "Body rows", "Remove 18 first rows");
+
+ Assert.assertTrue(
+ "All newly required rows should be fetched in the same round trip.",
+ logContainsText("Requested items 37 - 55"));
+
+ selectMenuPath("Settings", "Clear log");
+ selectMenuPath("Component", "Body rows", "Remove 18 first rows");
+
+ Assert.assertTrue(
+ "All newly required rows should be fetched in the same round trip.",
+ logContainsText("Requested items 37 - 55"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java
new file mode 100644
index 0000000000..3dbf613ba0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.GridElement.GridRowElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridSelectionTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testSelectOnOff() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+ toggleFirstRowSelection();
+ assertTrue("row should become selected", getRow(0).isSelected());
+ toggleFirstRowSelection();
+ assertFalse("row shouldn't remain selected", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSelectOnScrollOffScroll() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+ toggleFirstRowSelection();
+ assertTrue("row should become selected", getRow(0).isSelected());
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ scrollGridVerticallyTo(0); // scroll it back into view
+
+ assertTrue("row should still be selected when scrolling "
+ + "back into view", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSelectScrollOnScrollOff() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ toggleFirstRowSelection();
+
+ scrollGridVerticallyTo(0); // scroll it back into view
+ assertTrue("row should still be selected when scrolling "
+ + "back into view", getRow(0).isSelected());
+
+ toggleFirstRowSelection();
+ assertFalse("row shouldn't remain selected", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSelectScrollOnOffScroll() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ toggleFirstRowSelection();
+ toggleFirstRowSelection();
+
+ scrollGridVerticallyTo(0); // make sure the row is out of cache
+ assertFalse("row shouldn't be selected when scrolling "
+ + "back into view", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSingleSelectionUpdatesFromServer() {
+ openTestURL();
+ setSelectionModelSingle();
+
+ GridElement grid = getGridElement();
+ assertFalse("First row was selected from start", grid.getRow(0)
+ .isSelected());
+ toggleFirstRowSelection();
+ assertTrue("First row was not selected.", getRow(0).isSelected());
+ assertTrue("Selection event was not correct",
+ logContainsText("Added 0, Removed none"));
+ grid.getCell(5, 0).click();
+ assertTrue("Fifth row was not selected.", getRow(5).isSelected());
+ assertFalse("First row was still selected.", getRow(0).isSelected());
+ assertTrue("Selection event was not correct",
+ logContainsText("Added 5, Removed 0"));
+ grid.getCell(0, 6).click();
+ assertTrue("Selection event was not correct",
+ logContainsText("Added 0, Removed 5"));
+ toggleFirstRowSelection();
+ assertTrue("Selection event was not correct",
+ logContainsText("Added none, Removed 0"));
+ assertFalse("First row was still selected.", getRow(0).isSelected());
+ assertFalse("Fifth row was still selected.", getRow(5).isSelected());
+
+ grid.scrollToRow(600);
+ grid.getCell(595, 3).click();
+ assertTrue("Row 595 was not selected.", getRow(595).isSelected());
+ assertTrue("Selection event was not correct",
+ logContainsText("Added 595, Removed none"));
+ toggleFirstRowSelection();
+ assertFalse("Row 595 was still selected.", getRow(595).isSelected());
+ assertTrue("First row was not selected.", getRow(0).isSelected());
+ assertTrue("Selection event was not correct",
+ logContainsText("Added 0, Removed 595"));
+ }
+
+ @Test
+ public void testKeyboardSelection() {
+ openTestURL();
+ setSelectionModelMulti();
+
+ GridElement grid = getGridElement();
+ grid.getCell(3, 1).click();
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not selected with space key.", grid
+ .getRow(3).isSelected());
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not deselected with space key.", !grid
+ .getRow(3).isSelected());
+
+ grid.scrollToRow(500);
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not selected with space key.", grid
+ .getRow(3).isSelected());
+ }
+
+ @Test
+ public void testKeyboardWithSingleSelection() {
+ openTestURL();
+ setSelectionModelSingle();
+
+ GridElement grid = getGridElement();
+ grid.getCell(3, 1).click();
+
+ assertTrue("Grid row 3 was not selected with clicking.", grid.getRow(3)
+ .isSelected());
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not deselected with space key.", !grid
+ .getRow(3).isSelected());
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not selected with space key.", grid
+ .getRow(3).isSelected());
+
+ grid.scrollToRow(500);
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not deselected with space key.", !grid
+ .getRow(3).isSelected());
+ }
+
+ @Test
+ public void testSelectAllCheckbox() {
+ openTestURL();
+
+ setSelectionModelMulti();
+ GridCellElement header = getGridElement().getHeaderCell(0, 0);
+
+ assertTrue("No checkbox", header.isElementPresent(By.tagName("input")));
+ header.findElement(By.tagName("input")).click();
+
+ for (int i = 0; i < GridBasicFeatures.ROWS; i += 100) {
+ assertTrue("Row " + i + " was not selected.", getGridElement()
+ .getRow(i).isSelected());
+ }
+
+ header.findElement(By.tagName("input")).click();
+ assertFalse("Row 100 was still selected", getGridElement().getRow(100)
+ .isSelected());
+ }
+
+ @Test
+ public void testSelectAllAndSort() {
+ openTestURL();
+
+ setSelectionModelMulti();
+ GridCellElement header = getGridElement().getHeaderCell(0, 0);
+
+ header.findElement(By.tagName("input")).click();
+
+ getGridElement().getHeaderCell(0, 1).click();
+
+ WebElement selectionBox = getGridElement().getCell(4, 0).findElement(
+ By.tagName("input"));
+ selectionBox.click();
+ selectionBox.click();
+
+ assertFalse(
+ "Exception occured on row reselection.",
+ logContainsText("Exception occured, java.lang.IllegalStateException: No item id for key 101 found."));
+ }
+
+ @Test
+ public void testSelectAllCheckboxWhenChangingModels() {
+ openTestURL();
+
+ GridCellElement header;
+ header = getGridElement().getHeaderCell(0, 0);
+ assertFalse(
+ "Check box shouldn't have been in header for None Selection Model",
+ header.isElementPresent(By.tagName("input")));
+
+ setSelectionModelMulti();
+ header = getGridElement().getHeaderCell(0, 0);
+ assertTrue("Multi Selection Model should have select all checkbox",
+ header.isElementPresent(By.tagName("input")));
+
+ setSelectionModelSingle();
+ header = getGridElement().getHeaderCell(0, 0);
+ assertFalse(
+ "Check box shouldn't have been in header for Single Selection Model",
+ header.isElementPresent(By.tagName("input")));
+
+ // Single selection model shouldn't have selection column to begin with
+ assertFalse(
+ "Selection columnn shouldn't have been in grid for Single Selection Model",
+ getGridElement().getCell(0, 1).isElementPresent(
+ By.tagName("input")));
+
+ setSelectionModelNone();
+ header = getGridElement().getHeaderCell(0, 0);
+ assertFalse(
+ "Check box shouldn't have been in header for None Selection Model",
+ header.isElementPresent(By.tagName("input")));
+
+ }
+
+ private void setSelectionModelMulti() {
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+ }
+
+ private void setSelectionModelSingle() {
+ selectMenuPath("Component", "State", "Selection mode", "single");
+ }
+
+ private void setSelectionModelNone() {
+ selectMenuPath("Component", "State", "Selection mode", "none");
+ }
+
+ private void toggleFirstRowSelection() {
+ selectMenuPath("Component", "Body rows", "Select first row");
+ }
+
+ private GridRowElement getRow(int i) {
+ return getGridElement().getRow(i);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java
new file mode 100644
index 0000000000..7e805595c6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+@TestCategory("grid")
+public class GridSortingTest extends GridBasicFeaturesTest {
+
+ private static class SortInfo {
+ public final int sortOrder;
+ public final SortDirection sortDirection;
+
+ private SortInfo(int sortOrder, SortDirection sortDirection) {
+ this.sortOrder = sortOrder;
+ this.sortDirection = sortDirection;
+ }
+ }
+
+ private static class SortInfoWithColumn extends SortInfo {
+ public final int columnIndex;
+
+ private SortInfoWithColumn(int columnIndex, int sortOrder,
+ SortDirection sortDirection) {
+ super(sortOrder, sortDirection);
+ this.columnIndex = columnIndex;
+ }
+ }
+
+ private static SortInfo _(int sortOrder, SortDirection sortDirection) {
+ return new SortInfo(sortOrder, sortDirection);
+ }
+
+ private static SortInfoWithColumn _(int columnIndex, int sortOrder,
+ SortDirection sortDirection) {
+ return new SortInfoWithColumn(columnIndex, sortOrder, sortDirection);
+ }
+
+ @Test
+ public void testProgrammaticSorting() throws Exception {
+ openTestURL();
+
+ // Sorting by column 9 is sorting by row index that is represented as a
+ // String.
+ // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0)
+ sortBy("Column 9, DESC");
+ assertLastSortIsUserOriginated(false);
+
+ // Verify that programmatic sorting calls are identified as originating
+ // from API
+ assertColumnsAreSortedAs(_(9, 1, SortDirection.DESCENDING));
+
+ String row = "";
+ for (int i = 0; i < 3; ++i) {
+ row += "9";
+ String expected = "(" + row + ", 0)";
+ String cellValue = getGridElement().getCell(i, 0).getText();
+ assertEquals("Grid is not sorted by Column 9 "
+ + "using descending direction.", expected, cellValue);
+ }
+
+ // Column 10 is random numbers from Random with seed 13334
+ sortBy("Column 10, ASC");
+
+ assertFalse("Column 9 should no longer have the sort-desc stylename",
+ getGridElement().getHeaderCell(0, 9).getAttribute("class")
+ .contains("sort-desc"));
+
+ assertColumnsAreSortedAs(_(10, 1, SortDirection.ASCENDING));
+
+ for (int i = 0; i < 5; ++i) {
+ Integer firstRow = Integer.valueOf(getGridElement().getCell(i + 1,
+ 10).getText());
+ Integer secondRow = Integer.valueOf(getGridElement().getCell(i, 10)
+ .getText());
+ assertGreater("Grid is not sorted by Column 10 using"
+ + " ascending direction", firstRow, secondRow);
+
+ }
+
+ // Column 7 is row index as a number. Last three row are original rows
+ // 2, 1 and 0.
+ sortBy("Column 7, DESC");
+ for (int i = 0; i < 3; ++i) {
+ String expected = "(" + i + ", 0)";
+ String cellContent = getGridElement().getCell(
+ GridBasicFeatures.ROWS - (i + 1), 0).getText();
+ assertEquals("Grid is not sorted by Column 7 using "
+ + "descending direction", expected, cellContent);
+ }
+
+ assertFalse("Column 10 should no longer have the sort-asc stylename",
+ getGridElement().getHeaderCell(0, 10).getAttribute("class")
+ .contains("sort-asc"));
+
+ assertColumnsAreSortedAs(_(7, 1, SortDirection.DESCENDING));
+ }
+
+ @Test
+ public void testMouseSorting() throws Exception {
+ setDebug(true);
+ openTestURL();
+
+ GridElement grid = getGridElement();
+
+ selectMenuPath("Component", "Columns", "Column 9", "Column 9 Width",
+ "Auto");
+
+ // Sorting by column 9 is sorting by row index that is represented as a
+ // String.
+
+ // Click header twice to sort descending
+ GridCellElement header = grid.getHeaderCell(0, 9);
+ header.click();
+ assertLastSortIsUserOriginated(true);
+
+ assertColumnsAreSortedAs(_(9, 1, SortDirection.ASCENDING));
+ grid.getHeaderCell(0, 9).click();
+ assertColumnsAreSortedAs(_(9, 1, SortDirection.DESCENDING));
+
+ // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0)
+ String row = "";
+ for (int i = 0; i < 3; ++i) {
+ row += "9";
+ String expected = "(" + row + ", 0)";
+ String actual = grid.getCell(i, 0).getText();
+ assertEquals("Grid is not sorted by Column 9"
+ + " using descending direction.", expected, actual);
+ }
+
+ selectMenuPath("Component", "Columns", "Column 10", "Column 10 Width",
+ "Auto");
+ // Column 10 is random numbers from Random with seed 13334
+ // Click header to sort ascending
+ grid.getHeaderCell(0, 10).click();
+ assertColumnsAreSortedAs(_(10, 1, SortDirection.ASCENDING));
+
+ for (int i = 0; i < 5; ++i) {
+ Integer firstRow = Integer.valueOf(grid.getCell(i + 1, 10)
+ .getText());
+ Integer secondRow = Integer.valueOf(grid.getCell(i, 10).getText());
+ assertGreater(
+ "Grid is not sorted by Column 10 using ascending direction",
+ firstRow, secondRow);
+
+ }
+
+ selectMenuPath("Component", "Columns", "Column 7", "Column 7 Width",
+ "Auto");
+ // Column 7 is row index as a number. Last three row are original rows
+ // 2, 1 and 0.
+ // Click header twice to sort descending
+ grid.getHeaderCell(0, 7).click();
+ assertColumnsAreSortedAs(_(7, 1, SortDirection.ASCENDING));
+ grid.getHeaderCell(0, 7).click();
+ assertColumnsAreSortedAs(_(7, 1, SortDirection.DESCENDING));
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals(
+ "Grid is not sorted by Column 7 using descending direction",
+ "(" + i + ", 0)",
+ grid.getCell(GridBasicFeatures.ROWS - (i + 1), 0).getText());
+ }
+
+ }
+
+ private void sendKey(Keys seq) {
+ new Actions(getDriver()).sendKeys(seq).perform();
+ }
+
+ private void holdKey(Keys key) {
+ new Actions(getDriver()).keyDown(key).perform();
+ }
+
+ private void releaseKey(Keys key) {
+ new Actions(getDriver()).keyUp(key).perform();
+ }
+
+ @Test
+ public void testKeyboardSorting() {
+ openTestURL();
+
+ /*
+ * We can't click on the header directly, since it will sort the header
+ * immediately. We need to focus some other column first, and only then
+ * navigate there.
+ */
+ getGridElement().getCell(0, 0).click();
+ sendKey(Keys.ARROW_UP);
+
+ // Sort ASCENDING on first column
+ sendKey(Keys.ENTER);
+ assertLastSortIsUserOriginated(true);
+ assertColumnsAreSortedAs(_(1, SortDirection.ASCENDING));
+
+ // Move to next column
+ sendKey(Keys.RIGHT);
+
+ // Add this column to the existing sorting group
+ holdKey(Keys.SHIFT);
+ sendKey(Keys.ENTER);
+ releaseKey(Keys.SHIFT);
+ assertColumnsAreSortedAs(_(1, SortDirection.ASCENDING),
+ _(2, SortDirection.ASCENDING));
+
+ // Move to next column
+ sendKey(Keys.RIGHT);
+
+ // Add a third column to the sorting group
+ holdKey(Keys.SHIFT);
+ sendKey(Keys.ENTER);
+ releaseKey(Keys.SHIFT);
+ assertColumnsAreSortedAs(_(1, SortDirection.ASCENDING),
+ _(2, SortDirection.ASCENDING), _(3, SortDirection.ASCENDING));
+
+ // Move back to the second column
+ sendKey(Keys.LEFT);
+
+ // Change sort direction of the second column to DESCENDING
+ holdKey(Keys.SHIFT);
+ sendKey(Keys.ENTER);
+ releaseKey(Keys.SHIFT);
+ assertColumnsAreSortedAs(_(1, SortDirection.ASCENDING),
+ _(2, SortDirection.DESCENDING), _(3, SortDirection.ASCENDING));
+
+ // Move back to the third column
+ sendKey(Keys.RIGHT);
+
+ // Set sorting to third column, ASCENDING
+ sendKey(Keys.ENTER);
+ assertColumnsAreSortedAs(_(2, 1, SortDirection.ASCENDING));
+
+ // Move to the fourth column
+ sendKey(Keys.RIGHT);
+
+ // Make sure that single-column sorting also works as expected
+ sendKey(Keys.ENTER);
+ assertColumnsAreSortedAs(_(3, 1, SortDirection.ASCENDING));
+
+ }
+
+ private void assertColumnsAreSortedAs(SortInfoWithColumn... sortInfos) {
+ for (SortInfoWithColumn sortInfo : sortInfos) {
+ assertSort(sortInfo, sortInfo.columnIndex,
+ onlyOneColumnIsSorted(sortInfos));
+ }
+ }
+
+ /**
+ * @param sortDirections
+ * <code>null</code> if not interested in that index, otherwise a
+ * direction that the column needs to be sorted as
+ */
+ private void assertColumnsAreSortedAs(SortInfo... sortInfos) {
+ for (int column = 0; column < sortInfos.length; column++) {
+ SortInfo sortInfo = sortInfos[column];
+ assertSort(sortInfo, column, onlyOneColumnIsSorted(sortInfos));
+ }
+ }
+
+ private void assertSort(SortInfo sortInfo, int column,
+ boolean onlyOneColumnIsSorted) {
+ if (sortInfo == null) {
+ return;
+ }
+
+ GridCellElement headerCell = getGridElement().getHeaderCell(0, column);
+ String classValue = headerCell.getAttribute("class");
+
+ boolean isSortedAscending = sortInfo.sortDirection == SortDirection.ASCENDING
+ && classValue.contains("sort-asc");
+ boolean isSortedDescending = sortInfo.sortDirection == SortDirection.DESCENDING
+ && classValue.contains("sort-desc");
+
+ if (isSortedAscending || isSortedDescending) {
+ String sortOrderAttribute = headerCell.getAttribute("sort-order");
+
+ if (sortOrderAttribute == null) {
+ if (!(sortInfo.sortOrder == 1 && onlyOneColumnIsSorted)) {
+ fail("missing sort-order element attribute from column "
+ + column);
+ }
+ } else {
+ assertEquals("sort order was not as expected",
+ String.valueOf(sortInfo.sortOrder), sortOrderAttribute);
+ }
+ } else {
+ fail("column index " + column + " was not sorted as "
+ + sortInfo.sortDirection + " (class: " + classValue + ")");
+ }
+ }
+
+ private static boolean onlyOneColumnIsSorted(SortInfo[] sortInfos) {
+
+ boolean foundSortedColumn = false;
+ for (SortInfo sortInfo : sortInfos) {
+ if (sortInfo == null) {
+ continue;
+ }
+
+ if (!foundSortedColumn) {
+ foundSortedColumn = true;
+ } else {
+ // two columns were sorted
+ return false;
+ }
+ }
+ return foundSortedColumn;
+ }
+
+ private void sortBy(String column) {
+ selectMenuPath("Component", "State", "Sort by column", column);
+ }
+
+ private void assertLastSortIsUserOriginated(boolean isUserOriginated) {
+ List<WebElement> userOriginatedMessages = getDriver()
+ .findElements(
+ By.xpath("//*[contains(text(),'SortOrderChangeEvent: isUserOriginated')]"));
+
+ Collections.sort(userOriginatedMessages, new Comparator<WebElement>() {
+ @Override
+ public int compare(WebElement o1, WebElement o2) {
+ return o1.getText().compareTo(o2.getText());
+ }
+ });
+
+ String newestEntry = userOriginatedMessages.get(
+ userOriginatedMessages.size() - 1).getText();
+
+ String[] parts = newestEntry.split(" ");
+ boolean wasUserOriginated = Boolean
+ .parseBoolean(parts[parts.length - 1]);
+ if (isUserOriginated) {
+ assertTrue("expected the sort to be user originated, but wasn't",
+ wasUserOriginated);
+ } else {
+ assertFalse(
+ "expected the sort not to be user originated, but it was",
+ wasUserOriginated);
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java
new file mode 100644
index 0000000000..2fbaa58cab
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridStaticSectionComponentTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testNativeButtonInHeader() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 1", "Header Type",
+ "Widget Header");
+
+ getGridElement().$(ButtonElement.class).first().click();
+
+ assertTrue("Button click should be logged",
+ logContainsText("Button clicked!"));
+ }
+
+ @Test
+ public void testNativeButtonInFooter() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+ selectMenuPath("Component", "Footer", "Append row");
+ selectMenuPath("Component", "Columns", "Column 1", "Footer Type",
+ "Widget Footer");
+
+ getGridElement().$(ButtonElement.class).first().click();
+
+ assertTrue("Button click should be logged",
+ logContainsText("Button clicked!"));
+ }
+
+ @Test
+ public void testRemoveComponentFromHeader() throws Exception {
+ openTestURL();
+ selectMenuPath("Component", "Columns", "Column 1", "Header Type",
+ "Widget Header");
+ selectMenuPath("Component", "Columns", "Column 1", "Header Type",
+ "Text Header");
+ assertTrue("No notifications should've been shown",
+ !$(NotificationElement.class).exists());
+ assertEquals("Header should've been reverted back to text header",
+ "text header", getGridElement().getHeaderCell(0, 1).getText()
+ .toLowerCase());
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java
new file mode 100644
index 0000000000..08f903b3fe
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridStructureTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testRemovingAllColumns() {
+ setDebug(true);
+ openTestURL();
+ for (int i = 0; i < GridBasicFeatures.COLUMNS; ++i) {
+ selectMenuPath("Component", "Columns", "Column " + i,
+ "Add / Remove");
+ assertFalse(isElementPresent(NotificationElement.class));
+ }
+
+ assertEquals("Headers still visible.", 0, getGridHeaderRowCells()
+ .size());
+ }
+
+ @Test
+ public void testRemoveAndAddColumn() {
+ setDebug(true);
+ openTestURL();
+
+ assertEquals("column 0", getGridElement().getHeaderCell(0, 0).getText()
+ .toLowerCase());
+ selectMenuPath("Component", "Columns", "Column 0", "Add / Remove");
+ assertEquals("column 1", getGridElement().getHeaderCell(0, 0).getText()
+ .toLowerCase());
+ selectMenuPath("Component", "Columns", "Column 0", "Add / Remove");
+ // Column 0 is appended to the end of grid
+ assertEquals("column 0", getGridElement().getHeaderCell(0, 11)
+ .getText().toLowerCase());
+ }
+
+ @Test
+ public void testRemovingColumn() throws Exception {
+ openTestURL();
+
+ // Column 0 should be visible
+ List<TestBenchElement> cells = getGridHeaderRowCells();
+ assertEquals("column 0", cells.get(0).getText().toLowerCase());
+
+ // Hide column 0
+ selectMenuPath("Component", "Columns", "Column 0", "Add / Remove");
+
+ // Column 1 should now be the first cell
+ cells = getGridHeaderRowCells();
+ assertEquals("column 1", cells.get(0).getText().toLowerCase());
+ }
+
+ @Test
+ public void testDataLoadingAfterRowRemoval() throws Exception {
+ openTestURL();
+
+ // Remove columns 2,3,4
+ selectMenuPath("Component", "Columns", "Column 2", "Add / Remove");
+ selectMenuPath("Component", "Columns", "Column 3", "Add / Remove");
+ selectMenuPath("Component", "Columns", "Column 4", "Add / Remove");
+
+ // Scroll so new data is lazy loaded
+ scrollGridVerticallyTo(1000);
+
+ // Let lazy loading do its job
+ sleep(1000);
+
+ // Check that row is loaded
+ assertThat(getGridElement().getCell(11, 0).getText(), not("..."));
+ }
+
+ @Test
+ public void testFreezingColumn() throws Exception {
+ openTestURL();
+
+ // Freeze column 1
+ selectMenuPath("Component", "State", "Frozen column count", "1");
+
+ WebElement cell = getGridElement().getCell(0, 0);
+ assertTrue(cell.getAttribute("class").contains("frozen"));
+
+ cell = getGridElement().getCell(0, 1);
+ assertFalse(cell.getAttribute("class").contains("frozen"));
+ }
+
+ @Test
+ public void testInitialColumnWidths() throws Exception {
+ openTestURL();
+
+ WebElement cell = getGridElement().getCell(0, 0);
+ assertEquals(100, cell.getSize().getWidth());
+
+ cell = getGridElement().getCell(0, 1);
+ assertEquals(150, cell.getSize().getWidth());
+
+ cell = getGridElement().getCell(0, 2);
+ assertEquals(200, cell.getSize().getWidth());
+ }
+
+ @Test
+ public void testColumnWidths() throws Exception {
+ openTestURL();
+
+ // Default column width is 100px
+ WebElement cell = getGridElement().getCell(0, 0);
+ assertEquals(100, cell.getSize().getWidth());
+
+ // Set first column to be 200px wide
+ selectMenuPath("Component", "Columns", "Column 0", "Column 0 Width",
+ "200px");
+
+ cell = getGridElement().getCell(0, 0);
+ assertEquals(200, cell.getSize().getWidth());
+
+ // Set second column to be 150px wide
+ selectMenuPath("Component", "Columns", "Column 1", "Column 1 Width",
+ "150px");
+ cell = getGridElement().getCell(0, 1);
+ assertEquals(150, cell.getSize().getWidth());
+
+ selectMenuPath("Component", "Columns", "Column 0", "Column 0 Width",
+ "Auto");
+
+ // since the column 0 was previously 200, it should've shrunk when
+ // autoresizing.
+ cell = getGridElement().getCell(0, 0);
+ assertLessThan("", cell.getSize().getWidth(), 200);
+ }
+
+ @Test
+ public void testPrimaryStyleNames() throws Exception {
+ openTestURL();
+
+ // v-grid is default primary style namea
+ assertPrimaryStylename("v-grid");
+
+ selectMenuPath("Component", "State", "Primary style name",
+ "v-escalator");
+ assertPrimaryStylename("v-escalator");
+
+ selectMenuPath("Component", "State", "Primary style name", "my-grid");
+ assertPrimaryStylename("my-grid");
+
+ selectMenuPath("Component", "State", "Primary style name", "v-grid");
+ assertPrimaryStylename("v-grid");
+ }
+
+ /**
+ * Test that the current view is updated when a server-side container change
+ * occurs (without scrolling back and forth)
+ */
+ @Test
+ public void testItemSetChangeEvent() throws Exception {
+ openTestURL();
+
+ final org.openqa.selenium.By newRow = By
+ .xpath("//td[text()='newcell: 0']");
+
+ assertTrue("Unexpected initial state", !isElementPresent(newRow));
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertTrue("Add row failed", isElementPresent(newRow));
+
+ selectMenuPath("Component", "Body rows", "Remove first row");
+ assertTrue("Remove row failed", !isElementPresent(newRow));
+ }
+
+ /**
+ * Test that the current view is updated when a property's value is reflect
+ * to the client, when the value is modified server-side.
+ */
+ @Test
+ public void testPropertyValueChangeEvent() throws Exception {
+ openTestURL();
+
+ assertEquals("Unexpected cell initial state", "(0, 0)",
+ getGridElement().getCell(0, 0).getText());
+
+ selectMenuPath("Component", "Body rows",
+ "Modify first row (getItemProperty)");
+ assertEquals("(First) modification with getItemProperty failed",
+ "modified: 0", getGridElement().getCell(0, 0).getText());
+
+ selectMenuPath("Component", "Body rows",
+ "Modify first row (getContainerProperty)");
+ assertEquals("(Second) modification with getItemProperty failed",
+ "modified: Column 0", getGridElement().getCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRemovingAllItems() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Body rows", "Remove all rows");
+
+ assertEquals(0, getGridElement().findElement(By.tagName("tbody"))
+ .findElements(By.tagName("tr")).size());
+ }
+
+ @Test
+ public void testVerticalScrollBarVisibilityWhenEnoughRows()
+ throws Exception {
+ openTestURL();
+
+ assertTrue(verticalScrollbarIsPresent());
+
+ selectMenuPath("Component", "Body rows", "Remove all rows");
+ assertFalse(verticalScrollbarIsPresent());
+
+ selectMenuPath("Component", "Size", "HeightMode Row");
+ selectMenuPath("Component", "Size", "Height by Rows", "2.33 rows");
+ selectMenuPath("Component", "Body rows", "Add first row");
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertFalse(verticalScrollbarIsPresent());
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertTrue(verticalScrollbarIsPresent());
+ }
+
+ @Test
+ public void testBareItemSetChange() throws Exception {
+ openTestURL();
+ filterSomeAndAssert();
+ }
+
+ @Test
+ public void testBareItemSetChangeRemovingAllRows() throws Exception {
+ openTestURL();
+ selectMenuPath("Component", "Filter", "Add impassable filter");
+ assertFalse("A notification shouldn't have been displayed",
+ $(NotificationElement.class).exists());
+ assertTrue("No body cells should've been found", getGridElement()
+ .getBody().findElements(By.tagName("td")).isEmpty());
+ }
+
+ @Test
+ public void testBareItemSetChangeWithMidScroll() throws Exception {
+ openTestURL();
+ getGridElement().scrollToRow(GridBasicFeatures.ROWS / 2);
+ filterSomeAndAssert();
+ }
+
+ @Test
+ public void testBareItemSetChangeWithBottomScroll() throws Exception {
+ openTestURL();
+ getGridElement().scrollToRow(GridBasicFeatures.ROWS);
+ filterSomeAndAssert();
+ }
+
+ private void filterSomeAndAssert() {
+ selectMenuPath("Component", "Filter", "Column 1 starts with \"(23\"");
+ boolean foundElements = false;
+ for (int row = 0; row < 100; row++) {
+ try {
+ GridCellElement cell = getGridElement().getCell(row, 1);
+ foundElements = true;
+ assertTrue("Unexpected cell contents. "
+ + "Did the ItemSetChange work after all?", cell
+ .getText().startsWith("(23"));
+ } catch (NoSuchElementException e) {
+ assertTrue("No rows were found", foundElements);
+ return;
+ }
+ }
+ fail("unexpected amount of rows post-filter. Did the ItemSetChange work after all?");
+ }
+
+ @Test
+ public void testRemoveLastColumn() {
+ setDebug(true);
+ openTestURL();
+
+ String columnName = "Column " + (GridBasicFeatures.COLUMNS - 1);
+ assertTrue(columnName + " was not present in DOM",
+ isElementPresent(By.xpath("//th[text()='" + columnName + "']")));
+ selectMenuPath("Component", "Columns", columnName, "Add / Remove");
+ assertFalse(isElementPresent(NotificationElement.class));
+ assertFalse(columnName + " was still present in DOM",
+ isElementPresent(By.xpath("//th[text()='" + columnName + "']")));
+ }
+
+ @Test
+ public void testReverseColumns() {
+ openTestURL();
+
+ String[] gridData = new String[GridBasicFeatures.COLUMNS];
+ GridElement grid = getGridElement();
+ for (int i = 0; i < gridData.length; ++i) {
+ gridData[i] = grid.getCell(0, i).getAttribute("innerHTML");
+ }
+
+ selectMenuPath("Component", "State", "Reverse Grid Columns");
+
+ // Compare with reversed order
+ for (int i = 0; i < gridData.length; ++i) {
+ final int column = gridData.length - 1 - i;
+ final String newText = grid.getCell(0, column).getAttribute(
+ "innerHTML");
+ assertEquals("Grid contained unexpected values. (0, " + column
+ + ")", gridData[i], newText);
+ }
+ }
+
+ @Test
+ public void testAddingProperty() {
+ setDebug(true);
+ openTestURL();
+
+ assertNotEquals("property value", getGridElement().getCell(0, 0)
+ .getText());
+ selectMenuPath("Component", "Properties", "Prepend property");
+ assertEquals("property value", getGridElement().getCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRemovingAddedProperty() {
+ openTestURL();
+
+ assertEquals("(0, 0)", getGridElement().getCell(0, 0).getText());
+ assertNotEquals("property value", getGridElement().getCell(0, 0)
+ .getText());
+
+ selectMenuPath("Component", "Properties", "Prepend property");
+ selectMenuPath("Component", "Properties", "Prepend property");
+
+ assertNotEquals("property value", getGridElement().getCell(0, 0)
+ .getText());
+ assertEquals("(0, 0)", getGridElement().getCell(0, 0).getText());
+ }
+
+ private boolean verticalScrollbarIsPresent() {
+ return "scroll".equals(getGridVerticalScrollbar().getCssValue(
+ "overflow-y"));
+ }
+
+ @Test
+ public void testAddRowAboveViewport() {
+ setDebug(true);
+ openTestURL();
+
+ GridCellElement cell = getGridElement().getCell(500, 1);
+ String cellContent = cell.getText();
+ selectMenuPath("Component", "Body rows", "Add first row");
+
+ assertFalse("Error notification was present",
+ isElementPresent(NotificationElement.class));
+
+ assertEquals("Grid scrolled unexpectedly", cellContent, cell.getText());
+ }
+
+ @Test
+ public void testRemoveAndAddRowAboveViewport() {
+ setDebug(true);
+ openTestURL();
+
+ GridCellElement cell = getGridElement().getCell(500, 1);
+ String cellContent = cell.getText();
+ selectMenuPath("Component", "Body rows", "Remove first row");
+
+ assertFalse("Error notification was present after removing row",
+ isElementPresent(NotificationElement.class));
+
+ assertEquals("Grid scrolled unexpectedly", cellContent, cell.getText());
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+
+ assertFalse("Error notification was present after adding row",
+ isElementPresent(NotificationElement.class));
+
+ assertEquals("Grid scrolled unexpectedly", cellContent, cell.getText());
+ }
+
+ @Test
+ public void testScrollAndRemoveAll() {
+ setDebug(true);
+ openTestURL();
+
+ getGridElement().scrollToRow(500);
+ selectMenuPath("Component", "Body rows", "Remove all rows");
+
+ assertFalse("Error notification was present after removing all rows",
+ isElementPresent(NotificationElement.class));
+
+ assertFalse(getGridElement().isElementPresent(By.vaadin("#cell[0][0]")));
+ }
+
+ private void assertPrimaryStylename(String stylename) {
+ assertTrue(getGridElement().getAttribute("class").contains(stylename));
+
+ String tableWrapperStyleName = getGridElement().getTableWrapper()
+ .getAttribute("class");
+ assertTrue(tableWrapperStyleName.contains(stylename + "-tablewrapper"));
+
+ String hscrollStyleName = getGridElement().getHorizontalScroller()
+ .getAttribute("class");
+ assertTrue(hscrollStyleName.contains(stylename + "-scroller"));
+ assertTrue(hscrollStyleName
+ .contains(stylename + "-scroller-horizontal"));
+
+ String vscrollStyleName = getGridElement().getVerticalScroller()
+ .getAttribute("class");
+ assertTrue(vscrollStyleName.contains(stylename + "-scroller"));
+ assertTrue(vscrollStyleName.contains(stylename + "-scroller-vertical"));
+ }
+
+ @Test
+ public void testScrollPosDoesNotChangeAfterStateChange() {
+ openTestURL();
+ scrollGridVerticallyTo(1000);
+ int scrollPos = getGridVerticalScrollPos();
+ selectMenuPath("Component", "Editor", "Enabled");
+ assertEquals("Scroll position should've not have changed", scrollPos,
+ getGridVerticalScrollPos());
+ }
+
+ @Test
+ public void testReloadPage() throws InterruptedException {
+ setDebug(true);
+ openTestURL();
+
+ reopenTestURL();
+
+ // After opening the URL Grid can be stuck in a state where it thinks it
+ // should wait for something that's not going to happen.
+ testBench().disableWaitForVaadin();
+
+ // Wait until page is loaded completely.
+ int count = 0;
+ while (!isElementPresent(GridElement.class)) {
+ if (count == 100) {
+ fail("Reloading page failed");
+ }
+ sleep(100);
+ ++count;
+ }
+
+ // Wait a bit more for notification to occur.
+ sleep(1000);
+
+ assertFalse("Exception occurred when reloading page",
+ isElementPresent(NotificationElement.class));
+ }
+
+ @Test
+ public void testAddThirdRowToGrid() {
+ openTestURL();
+ selectMenuPath("Component", "Body rows", "Add third row");
+ assertFalse(logContainsText("Exception occured"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java
new file mode 100644
index 0000000000..b122eb02e9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.grid.basicfeatures.server;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import com.vaadin.testbench.elements.GridElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class LoadingIndicatorTest extends GridBasicFeaturesTest {
+ @Test
+ public void testLoadingIndicator() throws InterruptedException {
+ setDebug(true);
+ openTestURL();
+
+ selectMenuPath("Component", "State", "Container delay", "2000");
+
+ GridElement gridElement = $(GridElement.class).first();
+
+ Assert.assertFalse(
+ "Loading indicator should not be visible before disabling waitForVaadin",
+ isLoadingIndicatorVisible());
+
+ testBench().disableWaitForVaadin();
+
+ // Scroll to a completely new location
+ gridElement.getCell(200, 1);
+
+ // Wait for loading indicator delay
+ Thread.sleep(500);
+
+ Assert.assertTrue(
+ "Loading indicator should be visible when fetching rows that are visible",
+ isLoadingIndicatorVisible());
+
+ waitUntilNot(ExpectedConditions.visibilityOfElementLocated(By
+ .className("v-loading-indicator")));
+
+ // Scroll so much that more data gets fetched, but not so much that
+ // missing rows are shown
+ gridElement.getCell(230, 1);
+
+ // Wait for potentially triggered loading indicator to become visible
+ Thread.sleep(500);
+
+ Assert.assertFalse(
+ "Loading indicator should not be visible when fetching rows that are not visible",
+ isLoadingIndicatorVisible());
+
+ // Finally verify that there was actually a request going on
+ Thread.sleep(2000);
+
+ String firstLogRow = getLogRow(0);
+ Assert.assertTrue(
+ "Last log message should be number 6: " + firstLogRow,
+ firstLogRow.startsWith("6. Requested items"));
+ }
+
+ private boolean isLoadingIndicatorVisible() {
+ WebElement loadingIndicator = findElement(By
+ .className("v-loading-indicator"));
+ if (loadingIndicator == null) {
+ return false;
+ } else {
+ return loadingIndicator.isDisplayed();
+ }
+
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/myBeanJsRenderer.js b/uitest/src/com/vaadin/tests/components/grid/myBeanJsRenderer.js
new file mode 100644
index 0000000000..5e7bde5ec7
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/myBeanJsRenderer.js
@@ -0,0 +1,16 @@
+window.com_vaadin_tests_components_grid_MyBeanJSRenderer = function() {
+ this.init = function(cell) {
+ cell.element.setAttribute("column", cell.columnIndex);
+ }
+
+ this.render = function(cell, data) {
+ cell.element.innerHTML = 'Bean(' + data.integer + ', ' + data.string + ')'
+ }
+
+ this.getConsumedEvents = function() { return ["click"] };
+
+ this.onBrowserEvent = function(cell, event) {
+ cell.element.innerHTML = "Clicked " + cell.rowIndex + " with key " + this.getRowKey(cell.rowIndex) +" at " + event.clientX;
+ return true;
+ }
+} \ No newline at end of file
diff --git a/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeer.java b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeer.java
new file mode 100644
index 0000000000..6cf7fb0ded
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.progressindicator;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.ProgressBar;
+import com.vaadin.ui.themes.Reindeer;
+
+@Theme(Reindeer.THEME_NAME)
+public class ProgressBarStaticReindeer extends AbstractTestUI {
+ @Override
+ protected void setup(VaadinRequest request) {
+ ProgressBar progressBar = new ProgressBar();
+ progressBar.addStyleName(Reindeer.PROGRESSBAR_STATIC);
+ addComponent(progressBar);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeerTest.java b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeerTest.java
new file mode 100644
index 0000000000..f1056a640d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticReindeerTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.progressindicator;
+
+import org.junit.Test;
+
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class ProgressBarStaticReindeerTest extends MultiBrowserTest {
+ @Test
+ public void compareScreenshot() throws Exception {
+ openTestURL();
+ compareScreen("screen");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRuno.java b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRuno.java
new file mode 100644
index 0000000000..4e1ff7c886
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRuno.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.progressindicator;
+
+import com.vaadin.annotations.Theme;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.ProgressBar;
+import com.vaadin.ui.themes.Runo;
+
+@Theme(Runo.THEME_NAME)
+public class ProgressBarStaticRuno extends AbstractTestUI {
+ @Override
+ protected void setup(VaadinRequest request) {
+ ProgressBar progressBar = new ProgressBar();
+ progressBar.addStyleName(Runo.PROGRESSBAR_STATIC);
+ addComponent(progressBar);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRunoTest.java b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRunoTest.java
new file mode 100644
index 0000000000..751e048694
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/progressindicator/ProgressBarStaticRunoTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.components.progressindicator;
+
+import org.junit.Test;
+
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class ProgressBarStaticRunoTest extends MultiBrowserTest {
+ @Test
+ public void compareScreenshot() throws Exception {
+ openTestURL();
+ compareScreen("screen");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/serialization/NoLayout.java b/uitest/src/com/vaadin/tests/serialization/NoLayout.java
new file mode 100644
index 0000000000..8ce8c437a4
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/serialization/NoLayout.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.serialization;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.server.LayoutDetector;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.JavaScript;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class NoLayout extends AbstractTestUI {
+ private final LayoutDetector layoutDetector = new LayoutDetector();
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ addComponent(layoutDetector);
+
+ CheckBox uiPolling = new CheckBox("UI polling enabled");
+ uiPolling.addValueChangeListener(new ValueChangeListener() {
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ if (event.getProperty().getValue() == Boolean.TRUE) {
+ setPollInterval(100);
+ } else {
+ setPollInterval(-1);
+ }
+ }
+ });
+ addComponent(uiPolling);
+
+ addComponent(new Button("Change regular state",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ event.getButton().setCaption(
+ String.valueOf(System.currentTimeMillis()));
+ }
+ }));
+ addComponent(new Button("Change @NoLayout state",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ event.getButton().setDescription(
+ String.valueOf(System.currentTimeMillis()));
+ }
+ }));
+ addComponent(new Button("Do regular RPC", new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ JavaScript.eval("");
+ }
+ }));
+
+ addComponent(new Button("Do @NoLayout RPC", new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ layoutDetector.doNoLayoutRpc();
+ }
+ }));
+
+ addComponent(new Button("Update LegacyComponent",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ // Assumes UI is a LegacyComponent
+ markAsDirty();
+ }
+ }));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Checks which actions trigger a layout phase";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return Integer.valueOf(12936);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/serialization/NoLayoutTest.java b/uitest/src/com/vaadin/tests/serialization/NoLayoutTest.java
new file mode 100644
index 0000000000..bb312e3f3f
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/serialization/NoLayoutTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.serialization;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.CheckBoxElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class NoLayoutTest extends MultiBrowserTest {
+ @Test
+ public void testNoLayout() {
+ openTestURL();
+ assertCounts(1, 0);
+
+ $(CheckBoxElement.class).caption("UI polling enabled").first()
+ .findElement(By.tagName("input")).click();
+
+ // Toggling check box requires layout
+ assertCounts(2, 0);
+
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Count should not change even with polling enabled
+ assertCounts(2, 0);
+
+ // Disable polling
+ $(CheckBoxElement.class).caption("UI polling enabled").first()
+ .findElement(By.tagName("input")).click();
+ // Toggling checkbox layotus again
+ assertCounts(3, 0);
+
+ $(ButtonElement.class).caption("Change regular state").first().click();
+ // Updating normal state layouts
+ assertCounts(4, 0);
+
+ $(ButtonElement.class).caption("Change @NoLayout state").first();
+ // Updating @NoLayout state does not layout
+ assertCounts(4, 0);
+
+ $(ButtonElement.class).caption("Do regular RPC").first().click();
+ // Doing normal RPC layouts
+ assertCounts(5, 0);
+
+ $(ButtonElement.class).caption("Do @NoLayout RPC").first().click();
+ // Doing @NoLayout RPC does not layout, but updates the RPC count
+ assertCounts(5, 1);
+
+ $(ButtonElement.class).caption("Update LegacyComponent").first()
+ .click();
+ // Painting LegacyComponent layouts
+ assertCounts(6, 1);
+ }
+
+ private void assertCounts(int layoutCount, int rpcCount) {
+ Assert.assertEquals("Unexpected layout count", layoutCount,
+ getCount("layoutCount"));
+ Assert.assertEquals("Unexpected RPC count", rpcCount,
+ getCount("rpcCount"));
+ }
+
+ private int getCount(String id) {
+ return Integer.parseInt(getDriver().findElement(By.id(id)).getText());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/serialization/SerializerTest.java b/uitest/src/com/vaadin/tests/serialization/SerializerTest.java
index bb8b34d462..2ac10c161d 100644
--- a/uitest/src/com/vaadin/tests/serialization/SerializerTest.java
+++ b/uitest/src/com/vaadin/tests/serialization/SerializerTest.java
@@ -44,10 +44,14 @@ import com.vaadin.tests.widgetset.client.SerializerTestState;
import com.vaadin.tests.widgetset.client.SimpleTestBean;
import com.vaadin.tests.widgetset.server.SerializerTestExtension;
+import elemental.json.Json;
+import elemental.json.JsonString;
+import elemental.json.JsonValue;
+
@Widgetset("com.vaadin.tests.widgetset.TestingWidgetSet")
public class SerializerTest extends AbstractTestUI {
- private Log log = new Log(40);
+ private Log log = new Log(45);
@Override
protected void setup(VaadinRequest request) {
@@ -258,6 +262,12 @@ public class SerializerTest extends AbstractTestUI {
rpc.sendDate(new Date(1));
rpc.sendDate(new Date(2013 - 1900, 5 - 1, 31, 11, 12, 13));
+
+ state.jsonNull = Json.createNull();
+ state.jsonString = Json.create("a string");
+ state.jsonBoolean = Json.create(false);
+ rpc.sendJson(Json.create(true), Json.createNull(), Json.create("JSON"));
+
state.date1 = new Date(1);
state.date2 = new Date(2013 - 1900, 5 - 1, 31, 11, 12, 13);
@@ -458,6 +468,13 @@ public class SerializerTest extends AbstractTestUI {
}
@Override
+ public void sendJson(JsonValue value1, JsonValue value2,
+ JsonString string) {
+ log.log("sendJson: " + value1.toJson() + ", " + value2.toJson()
+ + ", " + string.toJson());
+ }
+
+ @Override
public void log(String string) {
log.log(string);
@@ -468,7 +485,7 @@ public class SerializerTest extends AbstractTestUI {
@Override
protected String getTestDescription() {
- return "Test for lots of different cases of encoding and decoding variuos data types";
+ return "Test for lots of different cases of encoding and decoding various data types";
}
@Override
diff --git a/uitest/src/com/vaadin/tests/serialization/SerializerTestTest.java b/uitest/src/com/vaadin/tests/serialization/SerializerTestTest.java
index 1624a89a01..23af74c78b 100644
--- a/uitest/src/com/vaadin/tests/serialization/SerializerTestTest.java
+++ b/uitest/src/com/vaadin/tests/serialization/SerializerTestTest.java
@@ -27,6 +27,9 @@ public class SerializerTestTest extends MultiBrowserTest {
openTestURL();
int logRow = 0;
+ Assert.assertEquals(
+ "sendJson: {\"b\":false,\"s\":\"JSON\"}, null, \"value\"",
+ getLogRow(logRow++));
Assert.assertEquals("sendDate: May 31, 2013 8:12:13 AM UTC",
getLogRow(logRow++));
Assert.assertEquals("sendDate: January 1, 1970 12:00:00 AM UTC",
@@ -77,6 +80,9 @@ public class SerializerTestTest extends MultiBrowserTest {
"sendBoolean: false, false, [false, false, true, false, true, true]",
getLogRow(logRow++));
Assert.assertEquals("sendBeanSubclass: 43", getLogRow(logRow++));
+ Assert.assertEquals("state.jsonBoolean: false", getLogRow(logRow++));
+ Assert.assertEquals("state.jsonString: a string", getLogRow(logRow++));
+ Assert.assertEquals("state.jsonNull: NULL", getLogRow(logRow++));
Assert.assertEquals(
"state.doubleArray: [1.7976931348623157e+308, 5e-324]",
getLogRow(logRow++));
diff --git a/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml b/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml
index 2c25c54e04..8a02d91d2c 100644
--- a/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml
+++ b/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml
@@ -4,6 +4,8 @@
<!-- Inherit the DefaultWidgetSet -->
<inherits name="com.vaadin.DefaultWidgetSet" />
+ <inherits name="com.google.gwt.user.theme.standard.Standard" />
+
<replace-with class="com.vaadin.tests.widgetset.client.CustomUIConnector">
<when-type-is class="com.vaadin.client.ui.ui.UIConnector" />
</replace-with>
@@ -17,5 +19,9 @@
class="com.vaadin.tests.widgetset.client.MockApplicationConnection">
<when-type-is class="com.vaadin.client.ApplicationConnection" />
</replace-with>
+
+ <generate-with class="com.vaadin.tests.widgetset.rebind.TestWidgetRegistryGenerator">
+ <when-type-is class="com.vaadin.tests.widgetset.client.TestWidgetConnector.TestWidgetRegistry" />
+ </generate-with>
</module>
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/BasicExtensionTestConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/BasicExtensionTestConnector.java
index 6bd2abec60..86c918536f 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/BasicExtensionTestConnector.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/BasicExtensionTestConnector.java
@@ -19,7 +19,6 @@ package com.vaadin.tests.widgetset.client;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.vaadin.client.ServerConnector;
-import com.vaadin.client.Util;
import com.vaadin.client.extensions.AbstractExtensionConnector;
import com.vaadin.shared.ui.Connect;
import com.vaadin.tests.extensions.BasicExtension;
@@ -35,8 +34,8 @@ public class BasicExtensionTestConnector extends AbstractExtensionConnector {
}
private void appendMessage(String action) {
- String message = Util.getSimpleName(this) + action
- + Util.getSimpleName(target);
+ String message = getClass().getSimpleName() + action
+ + target.getClass().getSimpleName();
DivElement element = Document.get().createDivElement();
element.setInnerText(message);
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/CustomUIConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/CustomUIConnector.java
index b43da8e27e..7a93f5e360 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/CustomUIConnector.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/CustomUIConnector.java
@@ -18,7 +18,6 @@ package com.vaadin.tests.widgetset.client;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.SpanElement;
-import com.vaadin.client.Util;
import com.vaadin.client.ui.ui.UIConnector;
import com.vaadin.shared.ui.Connect;
import com.vaadin.ui.UI;
@@ -33,7 +32,7 @@ public class CustomUIConnector extends UIConnector {
public void test() {
SpanElement span = Document.get().createSpanElement();
span.setInnerText("This is the "
- + Util.getSimpleName(CustomUIConnector.this));
+ + CustomUIConnector.this.getClass().getSimpleName());
Document.get().getBody().insertFirst(span);
}
});
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/LayoutDetectorConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/LayoutDetectorConnector.java
new file mode 100644
index 0000000000..e999c83b75
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/LayoutDetectorConnector.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client;
+
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.ui.PostLayoutListener;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.widgetset.server.LayoutDetector;
+
+@Connect(LayoutDetector.class)
+public class LayoutDetectorConnector extends AbstractComponentConnector
+ implements PostLayoutListener {
+ private int layoutCount = 0;
+ private int rpcCount = 0;
+
+ @Override
+ protected void init() {
+ super.init();
+ updateText();
+
+ registerRpc(NoLayoutRpc.class, new NoLayoutRpc() {
+ @Override
+ public void doRpc() {
+ rpcCount++;
+ updateText();
+ }
+ });
+ }
+
+ @Override
+ public HTML getWidget() {
+ return (HTML) super.getWidget();
+ }
+
+ @Override
+ public void postLayout() {
+ layoutCount++;
+ updateText();
+ }
+
+ private void updateText() {
+ getWidget().setHTML(
+ "Layout count: <span id='layoutCount'>" + layoutCount
+ + "</span><br />RPC count: <span id='rpcCount'>"
+ + rpcCount + "</span>");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java
index 4ee5b71387..0da1c6c775 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java
@@ -18,13 +18,14 @@ package com.vaadin.tests.widgetset.client;
import java.util.Date;
import java.util.logging.Logger;
-import com.google.gwt.json.client.JSONObject;
-import com.google.gwt.json.client.JSONValue;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ValueMap;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.tests.widgetset.server.csrf.ui.CsrfTokenDisabled;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
/**
* Mock ApplicationConnection for several issues where we need to hack it.
*
@@ -71,9 +72,9 @@ public class MockApplicationConnection extends ApplicationConnection {
}
@Override
- protected void doUidlRequest(String uri, JSONObject payload) {
- JSONValue jsonValue = payload.get(ApplicationConstants.CSRF_TOKEN);
- lastCsrfTokenSent = jsonValue != null ? jsonValue.toString() : null;
+ protected void doUidlRequest(String uri, JsonObject payload) {
+ JsonValue jsonValue = payload.get(ApplicationConstants.CSRF_TOKEN);
+ lastCsrfTokenSent = jsonValue != null ? jsonValue.toJson() : null;
super.doUidlRequest(uri, payload);
}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/NoLayoutRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/NoLayoutRpc.java
new file mode 100644
index 0000000000..7c2693db1d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/NoLayoutRpc.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client;
+
+import com.vaadin.shared.annotations.NoLayout;
+import com.vaadin.shared.communication.ClientRpc;
+
+public interface NoLayoutRpc extends ClientRpc {
+
+ @NoLayout
+ public void doRpc();
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/RoundTripTesterRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/RoundTripTesterRpc.java
index 60a3fb1448..e71f9f1230 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/RoundTripTesterRpc.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/RoundTripTesterRpc.java
@@ -15,10 +15,12 @@
*/
package com.vaadin.tests.widgetset.client;
+import com.vaadin.shared.annotations.NoLayout;
import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.ServerRpc;
public interface RoundTripTesterRpc extends ServerRpc, ClientRpc {
+ @NoLayout
public void ping(int nr, String payload);
public void done();
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestConnector.java
index 7758cdc2ac..07acd3d021 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestConnector.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestConnector.java
@@ -35,6 +35,13 @@ import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.label.ContentMode;
import com.vaadin.tests.widgetset.server.SerializerTestExtension;
+import elemental.json.Json;
+import elemental.json.JsonBoolean;
+import elemental.json.JsonObject;
+import elemental.json.JsonString;
+import elemental.json.JsonType;
+import elemental.json.JsonValue;
+
@Connect(SerializerTestExtension.class)
public class SerializerTestConnector extends AbstractExtensionConnector {
@@ -259,6 +266,27 @@ public class SerializerTestConnector extends AbstractExtensionConnector {
}
@Override
+ public void sendJson(JsonValue value1, JsonValue value2,
+ JsonString string) {
+ if (value1.getType() != JsonType.BOOLEAN) {
+ throw new RuntimeException("Expected boolean, got "
+ + value1.toJson());
+ }
+
+ if (value2.getType() != JsonType.NULL) {
+ throw new RuntimeException("Expected null, got "
+ + value2.toJson());
+ }
+
+ JsonObject returnObject = Json.createObject();
+ returnObject.put("b", !((JsonBoolean) value1).asBoolean());
+ returnObject.put("s", string);
+
+ rpc.sendJson(returnObject, Json.createNull(),
+ Json.create("value"));
+ }
+
+ @Override
public void log(String message) {
// Do nothing, used only in the other direction
}
@@ -311,6 +339,11 @@ public class SerializerTestConnector extends AbstractExtensionConnector {
rpc.log("state.doubleObjectValue: " + getState().doubleObjectValue);
rpc.log("state.doubleArray: " + Arrays.toString(getState().doubleArray));
+ rpc.log("state.jsonNull: " + getState().jsonNull.getType().name());
+ rpc.log("state.jsonString: "
+ + ((JsonString) getState().jsonString).getString());
+ rpc.log("state.jsonBoolean: " + getState().jsonBoolean.getBoolean());
+
/*
* TODO public double doubleValue; public Double DoubleValue; public
* double[] doubleArray; ;
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestRpc.java
index 6b4c4e7ac1..4baebc819e 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestRpc.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestRpc.java
@@ -26,6 +26,9 @@ import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.ui.label.ContentMode;
+import elemental.json.JsonString;
+import elemental.json.JsonValue;
+
@SuppressWarnings("javadoc")
public interface SerializerTestRpc extends ServerRpc, ClientRpc {
public void sendBoolean(boolean value, Boolean boxedValue, boolean[] array);
@@ -82,5 +85,7 @@ public interface SerializerTestRpc extends ServerRpc, ClientRpc {
public void sendDate(Date date);
+ public void sendJson(JsonValue value1, JsonValue value2, JsonString string);
+
public void log(String string);
}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestState.java b/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestState.java
index faf41fbf88..31ff58971f 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestState.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/SerializerTestState.java
@@ -24,6 +24,9 @@ import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.Connector;
import com.vaadin.shared.ui.label.ContentMode;
+import elemental.json.JsonBoolean;
+import elemental.json.JsonValue;
+
public class SerializerTestState extends AbstractComponentState {
public boolean booleanValue;
@@ -99,4 +102,8 @@ public class SerializerTestState extends AbstractComponentState {
public BeanWithAbstractSuperclass beanWithAbstractSuperclass;
+ public JsonValue jsonNull = null;
+ public JsonValue jsonString = null;
+ public JsonBoolean jsonBoolean = null;
+
}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/TestWidgetConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/TestWidgetConnector.java
new file mode 100644
index 0000000000..33a8956810
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/TestWidgetConnector.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.annotations.OnStateChange;
+import com.vaadin.client.metadata.Invoker;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.ui.SubPartAware;
+import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.widgetset.server.TestWidgetComponent;
+
+@Connect(TestWidgetComponent.class)
+public class TestWidgetConnector extends AbstractComponentConnector {
+ public static class SubPartAwareSimplePanel extends SimplePanel implements
+ SubPartAware {
+ @Override
+ public Element getSubPartElement(String subPart) {
+ Widget target = getWidget();
+ if (target instanceof SubPartAware) {
+ return ((SubPartAware) target).getSubPartElement(subPart);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ Widget target = getWidget();
+ if (target instanceof SubPartAware) {
+ return ((SubPartAware) target).getSubPartName(subElement);
+
+ } else {
+ return null;
+ }
+ }
+
+ }
+
+ public static class TestWidgetState extends AbstractComponentState {
+ public String widgetClass;
+ }
+
+ private final TestWidgetRegistry registry = GWT
+ .create(TestWidgetRegistry.class);
+
+ public static abstract class TestWidgetRegistry {
+ private Map<String, Invoker> creators = new HashMap<String, Invoker>();
+
+ // Called by generated sub class
+ protected void register(String widgetClass, Invoker creator) {
+ creators.put(widgetClass, creator);
+ }
+
+ public Widget createWidget(String widgetClass) {
+ Invoker invoker = creators.get(widgetClass);
+ if (invoker == null) {
+ return new Label("Widget not found: " + widgetClass);
+ } else {
+ return (Widget) invoker.invoke(null);
+ }
+ }
+ }
+
+ @OnStateChange("widgetClass")
+ private void updateWidgetClass() {
+ getWidget().setWidget(registry.createWidget(getState().widgetClass));
+ }
+
+ @Override
+ public TestWidgetState getState() {
+ return (TestWidgetState) super.getState();
+ }
+
+ @Override
+ public SubPartAwareSimplePanel getWidget() {
+ return (SubPartAwareSimplePanel) super.getWidget();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java
new file mode 100644
index 0000000000..761f32bc9a
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java
@@ -0,0 +1,702 @@
+package com.vaadin.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.client.widget.escalator.FlyweightCell;
+import com.vaadin.client.widget.escalator.Row;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widgets.Escalator;
+
+public class EscalatorBasicClientFeaturesWidget extends
+ PureGWTTestApplication<Escalator> {
+
+ public static class LogWidget extends Composite {
+
+ private static final int MAX_LOG = 9;
+
+ private final HTML html = new HTML();
+ private final List<String> logs = new ArrayList<String>();
+ private Escalator escalator;
+
+ public LogWidget() {
+ initWidget(html);
+ getElement().setId("log");
+ }
+
+ public void setEscalator(Escalator escalator) {
+ this.escalator = escalator;
+ }
+
+ public void updateDebugLabel() {
+ int headers = escalator.getHeader().getRowCount();
+ int bodys = escalator.getBody().getRowCount();
+ int footers = escalator.getFooter().getRowCount();
+ int columns = escalator.getColumnConfiguration().getColumnCount();
+
+ while (logs.size() > MAX_LOG) {
+ logs.remove(0);
+ }
+
+ String logString = "<hr>";
+ for (String log : logs) {
+ logString += log + "<br>";
+ }
+
+ html.setHTML("Columns: " + columns + "<br>" + //
+ "Header rows: " + headers + "<br>" + //
+ "Body rows: " + bodys + "<br>" + //
+ "Footer rows: " + footers + "<br>" + //
+ logString);
+ }
+
+ public void log(String string) {
+ logs.add((Duration.currentTimeMillis() % 10000) + ": " + string);
+ }
+ }
+
+ public static class UpdaterLifetimeWidget extends
+ EscalatorBasicClientFeaturesWidget {
+
+ private final EscalatorUpdater debugUpdater = new EscalatorUpdater() {
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ log("preAttach", cellsToAttach);
+ }
+
+ @Override
+ public void postAttach(Row row,
+ Iterable<FlyweightCell> attachedCells) {
+ log("postAttach", attachedCells);
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ log("update", cellsToUpdate);
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ log("preDetach", cellsToDetach);
+ }
+
+ @Override
+ public void postDetach(Row row,
+ Iterable<FlyweightCell> detachedCells) {
+ log("postDetach", detachedCells);
+ }
+
+ private void log(String methodName, Iterable<FlyweightCell> cells) {
+ if (!cells.iterator().hasNext()) {
+ return;
+ }
+
+ TableCellElement cellElement = cells.iterator().next()
+ .getElement();
+ boolean isAttached = cellElement.getParentElement() != null
+ && cellElement.getParentElement().getParentElement() != null;
+ logWidget.log(methodName + ": elementIsAttached == "
+ + isAttached);
+ }
+ };
+
+ public UpdaterLifetimeWidget() {
+ super();
+ escalator.getHeader().setEscalatorUpdater(debugUpdater);
+ escalator.getBody().setEscalatorUpdater(debugUpdater);
+ escalator.getFooter().setEscalatorUpdater(debugUpdater);
+ }
+ }
+
+ private static final String COLUMNS_AND_ROWS_MENU = "Columns and Rows";
+ private static final String GENERAL_MENU = "General";
+ private static final String FEATURES_MENU = "Features";
+
+ private static abstract class TestEscalatorUpdater implements
+ EscalatorUpdater {
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ // noop
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ // noop
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ // noop
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ // noop
+ }
+ }
+
+ private class Data {
+ private int columnCounter = 0;
+ private int rowCounter = 0;
+ private final List<Integer> columns = new ArrayList<Integer>();
+ private final List<Integer> rows = new ArrayList<Integer>();
+
+ @SuppressWarnings("boxing")
+ public void insertRows(final int offset, final int amount) {
+ final List<Integer> newRows = new ArrayList<Integer>();
+ for (int i = 0; i < amount; i++) {
+ newRows.add(rowCounter++);
+ }
+ rows.addAll(offset, newRows);
+ }
+
+ @SuppressWarnings("boxing")
+ public void insertColumns(final int offset, final int amount) {
+ final List<Integer> newColumns = new ArrayList<Integer>();
+ for (int i = 0; i < amount; i++) {
+ newColumns.add(columnCounter++);
+ }
+ columns.addAll(offset, newColumns);
+ }
+
+ public EscalatorUpdater createHeaderUpdater() {
+ return new TestEscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ for (final FlyweightCell cell : cellsToUpdate) {
+ final Integer columnName = columns
+ .get(cell.getColumn());
+ cell.getElement().setInnerText("Header " + columnName);
+
+ if (colspan == Colspan.NORMAL) {
+ if (cell.getColumn() % 2 == 0) {
+ cell.setColSpan(2);
+ }
+ } else if (colspan == Colspan.CRAZY) {
+ if (cell.getColumn() % 3 == 0) {
+ cell.setColSpan(2);
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public EscalatorUpdater createFooterUpdater() {
+ return new TestEscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ for (final FlyweightCell cell : cellsToUpdate) {
+ final Integer columnName = columns
+ .get(cell.getColumn());
+ cell.getElement().setInnerText("Footer " + columnName);
+
+ if (colspan == Colspan.NORMAL) {
+ if (cell.getColumn() % 2 == 0) {
+ cell.setColSpan(2);
+ }
+ } else if (colspan == Colspan.CRAZY) {
+ if (cell.getColumn() % 3 == 1) {
+ cell.setColSpan(2);
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public EscalatorUpdater createBodyUpdater() {
+ return new TestEscalatorUpdater() {
+
+ public void renderCell(final FlyweightCell cell) {
+ final Integer columnName = columns.get(cell.getColumn());
+ final Integer rowName = rows.get(cell.getRow());
+ String cellInfo = columnName + "," + rowName;
+
+ if (cell.getColumn() > 0) {
+ cell.getElement().setInnerText("Cell: " + cellInfo);
+ } else {
+ cell.getElement().setInnerText(
+ "Row " + cell.getRow() + ": " + cellInfo);
+ }
+
+ if (colspan == Colspan.NORMAL) {
+ if (cell.getColumn() % 2 == 0) {
+ cell.setColSpan(2);
+ }
+ } else if (colspan == Colspan.CRAZY) {
+ if (cell.getColumn() % 3 == cell.getRow() % 3) {
+ cell.setColSpan(2);
+ }
+ }
+ }
+
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ for (final FlyweightCell cell : cellsToUpdate) {
+ renderCell(cell);
+ }
+ }
+ };
+ }
+
+ public void removeRows(final int offset, final int amount) {
+ for (int i = 0; i < amount; i++) {
+ rows.remove(offset);
+ }
+ }
+
+ public void removeColumns(final int offset, final int amount) {
+ for (int i = 0; i < amount; i++) {
+ columns.remove(offset);
+ }
+ }
+ }
+
+ protected final Escalator escalator;
+ private final Data data = new Data();
+
+ private enum Colspan {
+ NONE, NORMAL, CRAZY;
+ }
+
+ private Colspan colspan = Colspan.NONE;
+ protected final LogWidget logWidget = new LogWidget();
+
+ public EscalatorBasicClientFeaturesWidget() {
+ super(new EscalatorProxy());
+ escalator = getTestedWidget();
+ logWidget.setEscalator(escalator);
+
+ ((EscalatorProxy) escalator).setLogWidget(logWidget);
+ addNorth(logWidget, 200);
+
+ final RowContainer header = escalator.getHeader();
+ header.setEscalatorUpdater(data.createHeaderUpdater());
+
+ final RowContainer footer = escalator.getFooter();
+ footer.setEscalatorUpdater(data.createFooterUpdater());
+
+ escalator.getBody().setEscalatorUpdater(data.createBodyUpdater());
+
+ setWidth("500px");
+ setHeight("500px");
+
+ escalator.getElement().getStyle().setZIndex(0);
+ addNorth(escalator, 500);
+
+ createGeneralMenu();
+ createColumnMenu();
+ createHeaderRowsMenu();
+ createBodyRowsMenu();
+ createFooterRowsMenu();
+ createColumnsAndRowsMenu();
+ createFrozenMenu();
+ createColspanMenu();
+ }
+
+ private void createFrozenMenu() {
+ String[] menupath = { FEATURES_MENU, "Frozen columns" };
+ addMenuCommand("Freeze 1 column", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ escalator.getColumnConfiguration().setFrozenColumnCount(1);
+ }
+ }, menupath);
+ addMenuCommand("Freeze 0 columns", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ escalator.getColumnConfiguration().setFrozenColumnCount(0);
+ }
+ }, menupath);
+ }
+
+ private void createColspanMenu() {
+ String[] menupath = { FEATURES_MENU, "Column spanning" };
+ addMenuCommand("Apply normal colspan", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ colspan = Colspan.NORMAL;
+ refreshEscalator();
+ }
+ }, menupath);
+ addMenuCommand("Apply crazy colspan", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ colspan = Colspan.CRAZY;
+ refreshEscalator();
+ }
+ }, menupath);
+ addMenuCommand("Apply no colspan", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ colspan = Colspan.NONE;
+ refreshEscalator();
+ }
+ }, menupath);
+ }
+
+ private void createColumnsAndRowsMenu() {
+ String[] menupath = { COLUMNS_AND_ROWS_MENU };
+ addMenuCommand("Add one of each row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertRows(escalator.getHeader(), 0, 1);
+ insertRows(escalator.getBody(), 0, 1);
+ insertRows(escalator.getFooter(), 0, 1);
+ }
+ }, menupath);
+ addMenuCommand("Remove one of each row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeRows(escalator.getHeader(), 0, 1);
+ removeRows(escalator.getBody(), 0, 1);
+ removeRows(escalator.getFooter(), 0, 1);
+ }
+ }, menupath);
+ }
+
+ private void createGeneralMenu() {
+ String[] menupath = { GENERAL_MENU };
+
+ addMenuCommand("Detach Escalator", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ escalator.removeFromParent();
+ }
+ }, menupath);
+
+ addMenuCommand("Attach Escalator", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (!escalator.isAttached()) {
+ addNorth(escalator, 500);
+ }
+ }
+ }, menupath);
+
+ addMenuCommand("Clear (columns, then rows)", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ resetColRow();
+ }
+ }, menupath);
+ addMenuCommand("Clear (rows, then columns)", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ resetRowCol();
+ }
+ }, menupath);
+ addMenuCommand("Populate Escalator (columns, then rows)",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ resetColRow();
+ insertColumns(0, 10);
+ insertRows(escalator.getHeader(), 0, 1);
+ insertRows(escalator.getBody(), 0, 100);
+ insertRows(escalator.getFooter(), 0, 1);
+ }
+ }, menupath);
+ addMenuCommand("Populate Escalator (rows, then columns)",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ resetColRow();
+ insertRows(escalator.getHeader(), 0, 1);
+ insertRows(escalator.getBody(), 0, 100);
+ insertRows(escalator.getFooter(), 0, 1);
+ insertColumns(0, 10);
+ }
+ }, menupath);
+
+ createSizeMenu();
+ }
+
+ private void createSizeMenu() {
+ String[] menupath = new String[] { "General", "Size" };
+
+ addSizeMenuItem(null, "height", menupath);
+ addSizeMenuItem("200px", "height", menupath);
+ addSizeMenuItem("400px", "height", menupath);
+ addSizeMenuItem(null, "width", menupath);
+ addSizeMenuItem("200px", "width", menupath);
+ addSizeMenuItem("400px", "width", menupath);
+ }
+
+ private void addSizeMenuItem(final String size, final String direction,
+ String[] menupath) {
+ final String title = (size != null ? size : "undefined");
+ addMenuCommand(title + " " + direction, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (direction.equals("height")) {
+ escalator.setHeight(size);
+ } else {
+ escalator.setWidth(size);
+ }
+ }
+ }, menupath);
+ }
+
+ private void createColumnMenu() {
+ String[] menupath = { COLUMNS_AND_ROWS_MENU, "Columns" };
+ addMenuCommand("Add one column to beginning", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertColumns(0, 1);
+ }
+ }, menupath);
+ addMenuCommand("Add one column to end", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertColumns(escalator.getColumnConfiguration()
+ .getColumnCount(), 1);
+ }
+ }, menupath);
+ addMenuCommand("Add ten columns", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertColumns(0, 10);
+ }
+ }, menupath);
+ addMenuCommand("Remove one column from beginning",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeColumns(0, 1);
+ }
+ }, menupath);
+ addMenuCommand("Remove one column from end", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeColumns(escalator.getColumnConfiguration()
+ .getColumnCount() - 1, 1);
+ }
+ }, menupath);
+
+ addMenuCommand("Refresh first column", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ escalator.getColumnConfiguration().refreshColumns(0, 1);
+ }
+ }, menupath);
+
+ addMenuCommand("Resize first column to max width",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ escalator.getColumnConfiguration()
+ .setColumnWidth(0, -1);
+ }
+ }, menupath);
+
+ addMenuCommand("Resize first column to 100 px", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ escalator.getColumnConfiguration().setColumnWidth(0, 100);
+ }
+ }, menupath);
+ }
+
+ private void createHeaderRowsMenu() {
+ String[] menupath = { COLUMNS_AND_ROWS_MENU, "Header Rows" };
+ createRowsMenu(escalator.getHeader(), menupath);
+ }
+
+ private void createFooterRowsMenu() {
+ String[] menupath = { COLUMNS_AND_ROWS_MENU, "Footer Rows" };
+ createRowsMenu(escalator.getFooter(), menupath);
+ }
+
+ private void createBodyRowsMenu() {
+ String[] menupath = { COLUMNS_AND_ROWS_MENU, "Body Rows" };
+ createRowsMenu(escalator.getBody(), menupath);
+
+ addMenuCommand("Add 5 rows to top", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertRows(escalator.getBody(), 0, 5);
+ }
+ }, menupath);
+ addMenuCommand("Add 50 rows to top", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertRows(escalator.getBody(), 0, 50);
+ }
+ }, menupath);
+ addMenuCommand("Remove 5 rows from bottom", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeRows(escalator.getBody(), escalator.getBody()
+ .getRowCount() - 5, 5);
+ }
+ }, menupath);
+ addMenuCommand("Remove 50 rows from bottom", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeRows(escalator.getBody(), escalator.getBody()
+ .getRowCount() - 50, 50);
+ }
+ }, menupath);
+ addMenuCommand("Remove 50 rows from almost bottom",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeRows(escalator.getBody(), escalator.getBody()
+ .getRowCount() - 60, 50);
+ }
+ }, menupath);
+ addMenuCommand("Remove all, insert 30 and scroll 40px",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ removeRows(escalator.getBody(), 0, escalator.getBody()
+ .getRowCount());
+ insertRows(escalator.getBody(), 0, 30);
+ escalator.setScrollTop(40);
+ }
+ }, menupath);
+ }
+
+ private void createRowsMenu(final RowContainer container, String[] menupath) {
+ addMenuCommand("Add one row to beginning", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ int offset = 0;
+ int number = 1;
+ insertRows(container, offset, number);
+ }
+ }, menupath);
+ addMenuCommand("Add one row to end", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ int offset = container.getRowCount();
+ int number = 1;
+ insertRows(container, offset, number);
+ }
+ }, menupath);
+ addMenuCommand("Remove one row from beginning", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ int offset = 0;
+ int number = 1;
+ removeRows(container, offset, number);
+ }
+ }, menupath);
+ addMenuCommand("Remove one row from end", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ int offset = container.getRowCount() - 1;
+ int number = 1;
+ removeRows(container, offset, number);
+ }
+ }, menupath);
+ addMenuCommand("Remove all rows", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (container.getRowCount() > 0) {
+ removeRows(container, 0, container.getRowCount());
+ }
+ }
+ }, menupath);
+ }
+
+ private void insertRows(final RowContainer container, int offset, int number) {
+ if (container == escalator.getBody()) {
+ data.insertRows(offset, number);
+ escalator.getBody().insertRows(offset, number);
+ } else {
+ container.insertRows(offset, number);
+ }
+ }
+
+ private void removeRows(final RowContainer container, int offset, int number) {
+ if (container == escalator.getBody()) {
+ data.removeRows(offset, number);
+ escalator.getBody().removeRows(offset, number);
+ } else {
+ container.removeRows(offset, number);
+ }
+ }
+
+ private void insertColumns(final int offset, final int number) {
+ data.insertColumns(offset, number);
+ escalator.getColumnConfiguration().insertColumns(offset, number);
+ }
+
+ private void removeColumns(final int offset, final int number) {
+ data.removeColumns(offset, number);
+ escalator.getColumnConfiguration().removeColumns(offset, number);
+ }
+
+ private void resetColRow() {
+ if (escalator.getColumnConfiguration().getColumnCount() > 0) {
+ removeColumns(0, escalator.getColumnConfiguration()
+ .getColumnCount());
+ }
+ if (escalator.getFooter().getRowCount() > 0) {
+ removeRows(escalator.getFooter(), 0, escalator.getFooter()
+ .getRowCount());
+ }
+
+ if (escalator.getBody().getRowCount() > 0) {
+ removeRows(escalator.getBody(), 0, escalator.getBody()
+ .getRowCount());
+ }
+
+ if (escalator.getHeader().getRowCount() > 0) {
+ removeRows(escalator.getHeader(), 0, escalator.getHeader()
+ .getRowCount());
+ }
+ }
+
+ private void resetRowCol() {
+ if (escalator.getFooter().getRowCount() > 0) {
+ removeRows(escalator.getFooter(), 0, escalator.getFooter()
+ .getRowCount());
+ }
+
+ if (escalator.getBody().getRowCount() > 0) {
+ removeRows(escalator.getBody(), 0, escalator.getBody()
+ .getRowCount());
+ }
+
+ if (escalator.getHeader().getRowCount() > 0) {
+ removeRows(escalator.getHeader(), 0, escalator.getHeader()
+ .getRowCount());
+ }
+
+ if (escalator.getColumnConfiguration().getColumnCount() > 0) {
+ removeColumns(0, escalator.getColumnConfiguration()
+ .getColumnCount());
+ }
+ }
+
+ private void refreshEscalator() {
+ if (escalator.getHeader().getRowCount() > 0) {
+ escalator.getHeader().refreshRows(0,
+ escalator.getHeader().getRowCount());
+ }
+
+ if (escalator.getBody().getRowCount() > 0) {
+ escalator.getBody().refreshRows(0,
+ escalator.getBody().getRowCount());
+ }
+
+ if (escalator.getFooter().getRowCount() > 0) {
+ escalator.getFooter().refreshRows(0,
+ escalator.getFooter().getRowCount());
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
new file mode 100644
index 0000000000..53bf96c587
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.TableRowElement;
+import com.vaadin.client.widget.escalator.Cell;
+import com.vaadin.client.widget.escalator.ColumnConfiguration;
+import com.vaadin.client.widget.escalator.EscalatorUpdater;
+import com.vaadin.client.widget.escalator.RowContainer;
+import com.vaadin.client.widgets.Escalator;
+import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget;
+
+public class EscalatorProxy extends Escalator {
+ private class ColumnConfigurationProxy implements ColumnConfiguration {
+ private ColumnConfiguration columnConfiguration;
+
+ public ColumnConfigurationProxy(ColumnConfiguration columnConfiguration) {
+ this.columnConfiguration = columnConfiguration;
+ }
+
+ @Override
+ public void removeColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ columnConfiguration.removeColumns(index, numberOfColumns);
+ logWidget.log("removeColumns " + index + ", " + numberOfColumns);
+ logWidget.updateDebugLabel();
+ }
+
+ @Override
+ public void insertColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ columnConfiguration.insertColumns(index, numberOfColumns);
+ logWidget.log("insertColumns " + index + ", " + numberOfColumns);
+ logWidget.updateDebugLabel();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columnConfiguration.getColumnCount();
+ }
+
+ @Override
+ public void setFrozenColumnCount(int count)
+ throws IllegalArgumentException {
+ columnConfiguration.setFrozenColumnCount(count);
+ }
+
+ @Override
+ public int getFrozenColumnCount() {
+ return columnConfiguration.getFrozenColumnCount();
+ }
+
+ @Override
+ public void setColumnWidth(int index, double px)
+ throws IllegalArgumentException {
+ columnConfiguration.setColumnWidth(index, px);
+ }
+
+ @Override
+ public double getColumnWidth(int index) throws IllegalArgumentException {
+ return columnConfiguration.getColumnWidth(index);
+ }
+
+ @Override
+ public double getColumnWidthActual(int index)
+ throws IllegalArgumentException {
+ return columnConfiguration.getColumnWidthActual(index);
+ }
+
+ @Override
+ public void refreshColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ columnConfiguration.refreshColumns(index, numberOfColumns);
+ }
+ }
+
+ private class RowContainerProxy implements RowContainer {
+ private final RowContainer rowContainer;
+
+ public RowContainerProxy(RowContainer rowContainer) {
+ this.rowContainer = rowContainer;
+ }
+
+ @Override
+ public EscalatorUpdater getEscalatorUpdater() {
+ return rowContainer.getEscalatorUpdater();
+ }
+
+ @Override
+ public void setEscalatorUpdater(EscalatorUpdater escalatorUpdater)
+ throws IllegalArgumentException {
+ rowContainer.setEscalatorUpdater(escalatorUpdater);
+ }
+
+ @Override
+ public void removeRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ rowContainer.removeRows(index, numberOfRows);
+ logWidget.log(rowContainer.getClass().getSimpleName()
+ + " removeRows " + index + ", " + numberOfRows);
+ logWidget.updateDebugLabel();
+ }
+
+ @Override
+ public void insertRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ rowContainer.insertRows(index, numberOfRows);
+ logWidget.log(rowContainer.getClass().getSimpleName()
+ + " insertRows " + index + ", " + numberOfRows);
+ logWidget.updateDebugLabel();
+ }
+
+ @Override
+ public void refreshRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ rowContainer.refreshRows(index, numberOfRows);
+ logWidget.log(rowContainer.getClass().getSimpleName()
+ + " refreshRows " + index + ", " + numberOfRows);
+ }
+
+ @Override
+ public int getRowCount() {
+ return rowContainer.getRowCount();
+ }
+
+ @Override
+ public void setDefaultRowHeight(double px) throws IllegalArgumentException {
+ rowContainer.setDefaultRowHeight(px);
+ }
+
+ @Override
+ public double getDefaultRowHeight() {
+ return rowContainer.getDefaultRowHeight();
+ }
+
+ @Override
+ public Cell getCell(Element element) {
+ return rowContainer.getCell(element);
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index)
+ throws IndexOutOfBoundsException, IllegalStateException {
+ return rowContainer.getRowElement(index);
+ }
+
+ @Override
+ public Element getElement() {
+ return rowContainer.getElement();
+ }
+
+ }
+
+ private RowContainer headerProxy = null;
+ private RowContainer bodyProxy = null;
+ private RowContainer footerProxy = null;
+ private ColumnConfiguration columnProxy = null;
+ private LogWidget logWidget;
+
+ @Override
+ public RowContainer getHeader() {
+ if (headerProxy == null) {
+ headerProxy = new RowContainerProxy(super.getHeader());
+ }
+ return headerProxy;
+ }
+
+ @Override
+ public RowContainer getFooter() {
+ if (footerProxy == null) {
+ footerProxy = new RowContainerProxy(super.getFooter());
+ }
+ return footerProxy;
+ }
+
+ @Override
+ public RowContainer getBody() {
+ if (bodyProxy == null) {
+ bodyProxy = new RowContainerProxy(super.getBody());
+ }
+ return bodyProxy;
+ }
+
+ @Override
+ public ColumnConfiguration getColumnConfiguration() {
+ if (columnProxy == null) {
+ columnProxy = new ColumnConfigurationProxy(
+ super.getColumnConfiguration());
+ }
+ return columnProxy;
+ }
+
+ public void setLogWidget(LogWidget logWidget) {
+ this.logWidget = logWidget;
+ logWidget.updateDebugLabel();
+ }
+
+ @Override
+ public void setScrollTop(double scrollTop) {
+ logWidget.log("setScrollTop " + scrollTop);
+ logWidget.updateDebugLabel();
+ super.setScrollTop(scrollTop);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java
new file mode 100644
index 0000000000..25a04b88f9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java
@@ -0,0 +1,1173 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+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.shared.HandlerRegistration;
+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.Composite;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TextBox;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.renderers.DateRenderer;
+import com.vaadin.client.renderers.HtmlRenderer;
+import com.vaadin.client.renderers.NumberRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.renderers.TextRenderer;
+import com.vaadin.client.ui.VLabel;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.CellStyleGenerator;
+import com.vaadin.client.widget.grid.EditorHandler;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.grid.RowReference;
+import com.vaadin.client.widget.grid.RowStyleGenerator;
+import com.vaadin.client.widget.grid.datasources.ListDataSource;
+import com.vaadin.client.widget.grid.datasources.ListSorter;
+import com.vaadin.client.widget.grid.events.BodyKeyDownHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyPressHandler;
+import com.vaadin.client.widget.grid.events.BodyKeyUpHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyDownHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyPressHandler;
+import com.vaadin.client.widget.grid.events.FooterKeyUpHandler;
+import com.vaadin.client.widget.grid.events.GridKeyDownEvent;
+import com.vaadin.client.widget.grid.events.GridKeyPressEvent;
+import com.vaadin.client.widget.grid.events.GridKeyUpEvent;
+import com.vaadin.client.widget.grid.events.HeaderKeyDownHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyPressHandler;
+import com.vaadin.client.widget.grid.events.HeaderKeyUpHandler;
+import com.vaadin.client.widget.grid.events.ScrollEvent;
+import com.vaadin.client.widget.grid.events.ScrollHandler;
+import com.vaadin.client.widget.grid.selection.SelectionModel.None;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.FooterRow;
+import com.vaadin.client.widgets.Grid.HeaderRow;
+import com.vaadin.client.widgets.Grid.SelectionMode;
+import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget.Data;
+
+/**
+ * Grid basic client features test application.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridBasicClientFeaturesWidget extends
+ PureGWTTestApplication<Grid<List<Data>>> {
+ public static final String ROW_STYLE_GENERATOR_NONE = "None";
+ public static final String ROW_STYLE_GENERATOR_ROW_INDEX = "Row numbers";
+ public static final String ROW_STYLE_GENERATOR_EVERY_THIRD = "Every third";
+
+ public static final String CELL_STYLE_GENERATOR_NONE = "None";
+ public static final String CELL_STYLE_GENERATOR_SIMPLE = "Simple";
+ public static final String CELL_STYLE_GENERATOR_COL_INDEX = "Column index";
+
+ public static enum Renderers {
+ TEXT_RENDERER, HTML_RENDERER, NUMBER_RENDERER, DATE_RENDERER;
+ }
+
+ private class TestEditorHandler implements EditorHandler<List<Data>> {
+
+ private Map<Grid.Column<?, ?>, TextBox> widgets = new HashMap<Grid.Column<?, ?>, TextBox>();
+
+ private Label log = new Label();
+
+ {
+ log.addStyleName("grid-editor-log");
+ addSouth(log, 20);
+ }
+
+ @Override
+ public void bind(EditorRequest<List<Data>> request) {
+ List<Data> rowData = ds.getRow(request.getRowIndex());
+
+ boolean hasSelectionColumn = !(grid.getSelectionModel() instanceof None);
+ for (int i = 0; i < rowData.size(); i++) {
+ int columnIndex = hasSelectionColumn ? i + 1 : i;
+ getWidget(columnIndex).setText(rowData.get(i).value.toString());
+ }
+ request.success();
+ }
+
+ @Override
+ public void cancel(EditorRequest<List<Data>> request) {
+ log.setText("Row " + request.getRowIndex() + " edit cancelled");
+ }
+
+ @Override
+ public void save(EditorRequest<List<Data>> request) {
+ try {
+ log.setText("Row " + request.getRowIndex() + " edit committed");
+ List<Data> rowData = ds.getRow(request.getRowIndex());
+
+ int i = 0;
+ for (; i < COLUMNS - MANUALLY_FORMATTED_COLUMNS; i++) {
+ rowData.get(i).value = getWidget(i).getText();
+ }
+
+ rowData.get(i).value = Integer
+ .valueOf(getWidget(i++).getText());
+ rowData.get(i).value = new Date(getWidget(i++).getText());
+ rowData.get(i).value = getWidget(i++).getText();
+ rowData.get(i).value = Integer
+ .valueOf(getWidget(i++).getText());
+ rowData.get(i).value = Integer
+ .valueOf(getWidget(i++).getText());
+
+ // notify data source of changes
+ ds.asList().set(request.getRowIndex(), rowData);
+ request.success();
+ } catch (Exception e) {
+ Logger.getLogger(getClass().getName()).warning(e.toString());
+ request.fail();
+ }
+ }
+
+ @Override
+ public TextBox getWidget(Grid.Column<?, List<Data>> column) {
+ if (grid.getColumns().indexOf(column) == 0
+ && !(grid.getSelectionModel() instanceof None)) {
+ return null;
+ }
+
+ TextBox w = widgets.get(column);
+ if (w == null) {
+ w = new TextBox();
+ w.getElement().getStyle().setMargin(0, Unit.PX);
+ widgets.put(column, w);
+ }
+ return w;
+ }
+
+ private TextBox getWidget(int i) {
+ return getWidget(grid.getColumn(i));
+ }
+ }
+
+ private static final int MANUALLY_FORMATTED_COLUMNS = 5;
+ public static final int COLUMNS = 12;
+ public static final int ROWS = 1000;
+
+ private final Grid<List<Data>> grid;
+ private List<List<Data>> data;
+ private final ListDataSource<List<Data>> ds;
+ private final ListSorter<List<Data>> sorter;
+
+ /**
+ * Our basic data object
+ */
+ public final static class Data {
+ Object value;
+ }
+
+ /**
+ * @since
+ * @return
+ */
+ private List<List<Data>> createData(int rowCount) {
+ List<List<Data>> dataList = new ArrayList<List<Data>>();
+ Random rand = new Random();
+ rand.setSeed(13334);
+ long timestamp = 0;
+ for (int row = 0; row < rowCount; row++) {
+
+ List<Data> datarow = createDataRow(COLUMNS);
+ dataList.add(datarow);
+ Data d;
+
+ int col = 0;
+ for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; ++col) {
+ d = datarow.get(col);
+ d.value = "(" + row + ", " + col + ")";
+ }
+
+ d = datarow.get(col++);
+ d.value = Integer.valueOf(row);
+
+ d = datarow.get(col++);
+ d.value = new Date(timestamp);
+ timestamp += 91250000; // a bit over a day, just to get
+ // variation
+
+ d = datarow.get(col++);
+ d.value = "<b>" + row + "</b>";
+
+ d = datarow.get(col++);
+ d.value = Integer.valueOf(rand.nextInt());
+
+ d = datarow.get(col++);
+ d.value = Integer.valueOf(rand.nextInt(5));
+ }
+
+ return dataList;
+ }
+
+ /**
+ * Convenience method for creating a list of Data objects to be used as a
+ * Row in the data source
+ *
+ * @param cols
+ * number of columns (items) to include in the row
+ * @return
+ */
+ private List<Data> createDataRow(int cols) {
+ List<Data> list = new ArrayList<Data>(cols);
+ for (int i = 0; i < cols; ++i) {
+ list.add(new Data());
+ }
+ return list;
+ }
+
+ @SuppressWarnings("unchecked")
+ public GridBasicClientFeaturesWidget() {
+ super(new Grid<List<Data>>());
+
+ // Initialize data source
+ data = createData(ROWS);
+
+ ds = new ListDataSource<List<Data>>(data);
+ grid = getTestedWidget();
+ grid.getElement().setId("testComponent");
+ grid.setDataSource(ds);
+ grid.addSelectAllHandler(ds.getSelectAllHandler());
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.setEditorHandler(new TestEditorHandler());
+
+ sorter = new ListSorter<List<Data>>(grid);
+
+ // Create a bunch of grid columns
+
+ // Data source layout:
+ // text (String) * (COLUMNS - MANUALLY_FORMATTED_COLUMNS + 1) |
+ // rownumber (Integer) | some date (Date) | row number as HTML (String)
+ // | random value (Integer)
+
+ int col = 0;
+
+ // Text times COLUMNS - MANUALLY_FORMATTED_COLUMNS
+ for (col = 0; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; ++col) {
+
+ final int c = col;
+
+ Grid.Column<String, List<Data>> column = new Grid.Column<String, List<Data>>(
+ createRenderer(Renderers.TEXT_RENDERER)) {
+ @Override
+ public String getValue(List<Data> row) {
+ return (String) row.get(c).value;
+ }
+ };
+
+ column.setWidth(50 + c * 25);
+ column.setHeaderCaption("Header (0," + c + ")");
+
+ grid.addColumn(column);
+ }
+
+ // Integer row number
+ {
+ final int c = col++;
+ Grid.Column<Integer, List<Data>> column = new Grid.Column<Integer, List<Data>>(
+ createRenderer(Renderers.NUMBER_RENDERER)) {
+ @Override
+ public Integer getValue(List<Data> row) {
+ return (Integer) row.get(c).value;
+ }
+ };
+ grid.addColumn(column);
+ column.setHeaderCaption("Header (0," + c + ")");
+ }
+
+ // Some date
+ {
+ final int c = col++;
+ Grid.Column<Date, List<Data>> column = new Grid.Column<Date, List<Data>>(
+ createRenderer(Renderers.DATE_RENDERER)) {
+ @Override
+ public Date getValue(List<Data> row) {
+ return (Date) row.get(c).value;
+ }
+ };
+ grid.addColumn(column);
+ column.setHeaderCaption("Header (0," + c + ")");
+ }
+
+ // Row number as a HTML string
+ {
+ final int c = col++;
+ Grid.Column<String, List<Data>> column = new Grid.Column<String, List<Data>>(
+ createRenderer(Renderers.HTML_RENDERER)) {
+ @Override
+ public String getValue(List<Data> row) {
+ return (String) row.get(c).value;
+ }
+ };
+ grid.addColumn(column);
+ column.setHeaderCaption("Header (0," + c + ")");
+ }
+
+ // Random integer value
+ {
+ final int c = col++;
+ Grid.Column<Integer, List<Data>> column = new Grid.Column<Integer, List<Data>>(
+ createRenderer(Renderers.NUMBER_RENDERER)) {
+ @Override
+ public Integer getValue(List<Data> row) {
+ return (Integer) row.get(c).value;
+ }
+ };
+ grid.addColumn(column);
+ column.setHeaderCaption("Header (0," + c + ")");
+ }
+
+ // Random integer value between 0 and 5
+ {
+ final int c = col++;
+ Grid.Column<Integer, List<Data>> column = new Grid.Column<Integer, List<Data>>(
+ createRenderer(Renderers.NUMBER_RENDERER)) {
+ @Override
+ public Integer getValue(List<Data> row) {
+ return (Integer) row.get(c).value;
+ }
+ };
+ grid.addColumn(column);
+ column.setHeaderCaption("Header (0," + c + ")");
+ }
+
+ HeaderRow row = grid.getDefaultHeaderRow();
+ for (int i = 0; i < col; ++i) {
+ String caption = "Header (0," + i + ")";
+ Grid.Column<?, ?> column = grid.getColumn(i);
+ // Lets use some different cell types
+ if (i % 3 == 0) {
+ // No-op
+ } else if (i % 2 == 0) {
+ row.getCell(column).setHtml("<b>" + caption + "</b>");
+ } else {
+ row.getCell(column).setWidget(new HTML(caption));
+ }
+ }
+ ++headerCounter;
+
+ //
+ // Populate the menu
+ //
+
+ createStateMenu();
+ createColumnsMenu();
+ createHeaderMenu();
+ createFooterMenu();
+ createEditorMenu();
+ createInternalsMenu();
+ createDataSourceMenu();
+
+ grid.getElement().getStyle().setZIndex(0);
+
+ //
+ // Composite wrapping for grid.
+ //
+ boolean isComposite = Window.Location.getParameter("composite") != null;
+ if (isComposite) {
+ addNorth(new Composite() {
+ {
+ initWidget(grid);
+ }
+ }, 400);
+ } else {
+ addNorth(grid, 400);
+ }
+
+ createKeyHandlers();
+ }
+
+ private void createInternalsMenu() {
+ String[] listenersPath = { "Component", "Internals", "Listeners" };
+ final Label label = new Label();
+ addSouth(label, 20);
+
+ addMenuCommand("Add scroll listener", new ScheduledCommand() {
+ private HandlerRegistration scrollHandler = null;
+
+ @Override
+ public void execute() {
+ if (scrollHandler != null) {
+ return;
+ }
+ scrollHandler = grid.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ @SuppressWarnings("hiding")
+ final Grid<?> grid = (Grid<?>) event.getSource();
+ label.setText("scrollTop: " + grid.getScrollTop()
+ + ", scrollLeft: " + grid.getScrollLeft());
+ }
+ });
+ }
+ }, listenersPath);
+ }
+
+ private void createStateMenu() {
+ String[] selectionModePath = { "Component", "State", "Selection mode" };
+ String[] primaryStyleNamePath = { "Component", "State",
+ "Primary Stylename" };
+ String[] rowStyleGeneratorNamePath = { "Component", "State",
+ "Row style generator" };
+ String[] cellStyleGeneratorNamePath = { "Component", "State",
+ "Cell style generator" };
+
+ addMenuCommand("multi", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ }
+ }, selectionModePath);
+
+ addMenuCommand("single", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ }
+ }, selectionModePath);
+
+ addMenuCommand("none", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ }
+ }, selectionModePath);
+
+ addMenuCommand("v-grid", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setStylePrimaryName("v-grid");
+
+ }
+ }, primaryStyleNamePath);
+
+ addMenuCommand("v-escalator", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setStylePrimaryName("v-escalator");
+
+ }
+ }, primaryStyleNamePath);
+
+ addMenuCommand("v-custom-style", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setStylePrimaryName("v-custom-style");
+
+ }
+ }, primaryStyleNamePath);
+
+ addMenuCommand("Edit and refresh Row 0", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ DataSource<List<Data>> ds = grid.getDataSource();
+ RowHandle<List<Data>> rowHandle = ds.getHandle(ds.getRow(0));
+ rowHandle.getRow().get(0).value = "Foo";
+ rowHandle.updateRow();
+ }
+ }, "Component", "State");
+
+ addMenuCommand("Delayed edit of Row 0", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ DataSource<List<Data>> ds = grid.getDataSource();
+ final RowHandle<List<Data>> rowHandle = ds.getHandle(ds
+ .getRow(0));
+
+ new Timer() {
+ @Override
+ public void run() {
+ rowHandle.getRow().get(0).value = "Bar";
+ rowHandle.updateRow();
+ }
+
+ }.schedule(5000);
+ }
+ }, "Component", "State");
+
+ addMenuCommand(ROW_STYLE_GENERATOR_NONE, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setRowStyleGenerator(null);
+ }
+ }, rowStyleGeneratorNamePath);
+
+ addMenuCommand(ROW_STYLE_GENERATOR_EVERY_THIRD, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setRowStyleGenerator(new RowStyleGenerator<List<Data>>() {
+
+ @Override
+ public String getStyle(RowReference<List<Data>> rowReference) {
+ if (rowReference.getRowIndex() % 3 == 0) {
+ return "third";
+ } else {
+ // First manual col is integer
+ Integer value = (Integer) rowReference.getRow()
+ .get(COLUMNS - MANUALLY_FORMATTED_COLUMNS).value;
+ return value.toString();
+ }
+ }
+ });
+
+ }
+ }, rowStyleGeneratorNamePath);
+
+ addMenuCommand(ROW_STYLE_GENERATOR_ROW_INDEX, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setRowStyleGenerator(new RowStyleGenerator<List<Data>>() {
+
+ @Override
+ public String getStyle(RowReference<List<Data>> rowReference) {
+ return Integer.toString(rowReference.getRowIndex());
+ }
+ });
+
+ }
+ }, rowStyleGeneratorNamePath);
+
+ addMenuCommand(CELL_STYLE_GENERATOR_NONE, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setCellStyleGenerator(null);
+ }
+ }, cellStyleGeneratorNamePath);
+
+ addMenuCommand(CELL_STYLE_GENERATOR_SIMPLE, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setCellStyleGenerator(new CellStyleGenerator<List<Data>>() {
+
+ @Override
+ public String getStyle(
+ CellReference<List<Data>> cellReference) {
+ Grid.Column<?, List<Data>> column = cellReference
+ .getColumn();
+ if (column == grid.getColumn(2)) {
+ return "two";
+ } else if (column == grid.getColumn(COLUMNS
+ - MANUALLY_FORMATTED_COLUMNS)) {
+ // First manual col is integer
+ Integer value = (Integer) column
+ .getValue(cellReference.getRow());
+ return value.toString();
+
+ } else {
+ return null;
+ }
+ }
+ });
+ }
+ }, cellStyleGeneratorNamePath);
+ addMenuCommand(CELL_STYLE_GENERATOR_COL_INDEX, new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setCellStyleGenerator(new CellStyleGenerator<List<Data>>() {
+
+ @Override
+ public String getStyle(
+ CellReference<List<Data>> cellReference) {
+ return cellReference.getRowIndex()
+ + "_"
+ + grid.getColumns().indexOf(
+ cellReference.getColumn());
+ }
+ });
+ }
+ }, cellStyleGeneratorNamePath);
+
+ for (int i = -1; i <= COLUMNS; i++) {
+ final int index = i;
+ // Including dummy "columns" prefix because TB fails to select item
+ // if it's too narrow
+ addMenuCommand(Integer.toString(index) + " columns",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setFrozenColumnCount(index);
+ }
+ }, "Component", "State", "Frozen column count");
+ }
+
+ addMenuCommand("Enabled", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ grid.setEnabled(!grid.isEnabled());
+ }
+ }, "Component", "State");
+ }
+
+ private void createColumnsMenu() {
+
+ for (int i = 0; i < COLUMNS; i++) {
+ final int index = i;
+ final Grid.Column<?, List<Data>> column = grid.getColumn(index);
+ addMenuCommand("Sortable", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ column.setSortable(!column.isSortable());
+ }
+ }, "Component", "Columns", "Column " + i);
+
+ addMenuCommand("auto", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ column.setWidth(-1);
+ }
+ }, "Component", "Columns", "Column " + i, "Width");
+ addMenuCommand("50px", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ column.setWidth(50);
+ }
+ }, "Component", "Columns", "Column " + i, "Width");
+ addMenuCommand("200px", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ column.setWidth(200);
+ }
+ }, "Component", "Columns", "Column " + i, "Width");
+
+ // Header types
+ addMenuCommand("Text Header", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ column.setHeaderCaption("Text Header");
+ }
+ }, "Component", "Columns", "Column " + i, "Header Type");
+ addMenuCommand("HTML Header", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getHeaderRow(0).getCell(column)
+ .setHtml("<b>HTML Header</b>");
+ }
+ }, "Component", "Columns", "Column " + i, "Header Type");
+ addMenuCommand("Widget Header", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ final Button button = new Button("Button Header");
+ button.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ button.setText("Clicked");
+ }
+ });
+ grid.getHeaderRow(0).getCell(column).setWidget(button);
+ }
+ }, "Component", "Columns", "Column " + i, "Header Type");
+
+ // Footer types
+ addMenuCommand("Text Footer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getFooterRow(0).getCell(column).setText("Text Footer");
+ }
+ }, "Component", "Columns", "Column " + i, "Footer Type");
+ addMenuCommand("HTML Footer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getFooterRow(0).getCell(column)
+ .setHtml("<b>HTML Footer</b>");
+ }
+ }, "Component", "Columns", "Column " + i, "Footer Type");
+ addMenuCommand("Widget Footer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ final Button button = new Button("Button Footer");
+ button.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ button.setText("Clicked");
+ }
+ });
+ grid.getFooterRow(0).getCell(column).setWidget(button);
+ }
+ }, "Component", "Columns", "Column " + i, "Footer Type");
+
+ // Renderer throwing exceptions
+ addMenuCommand("Broken renderer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ final Renderer<Object> originalRenderer = (Renderer<Object>) column
+ .getRenderer();
+
+ column.setRenderer(new Renderer<Object>() {
+ @Override
+ public void render(RendererCellReference cell,
+ Object data) {
+ if (cell.getRowIndex() == cell.getColumnIndex()) {
+ throw new RuntimeException("I'm broken");
+ }
+ originalRenderer.render(cell, data);
+ }
+ });
+ }
+ }, "Component", "Columns", "Column " + i);
+ }
+ }
+
+ private int headerCounter = 0;
+ private int footerCounter = 0;
+
+ private void setHeaderTexts(HeaderRow row) {
+ for (int i = 0; i < COLUMNS; ++i) {
+ String caption = "Header (" + headerCounter + "," + i + ")";
+
+ // Lets use some different cell types
+ if (i % 3 == 0) {
+ row.getCell(grid.getColumn(i)).setText(caption);
+ } else if (i % 2 == 0) {
+ row.getCell(grid.getColumn(i))
+ .setHtml("<b>" + caption + "</b>");
+ } else {
+ row.getCell(grid.getColumn(i)).setWidget(new HTML(caption));
+ }
+ }
+ headerCounter++;
+ }
+
+ private void setFooterTexts(FooterRow row) {
+ for (int i = 0; i < COLUMNS; ++i) {
+ String caption = "Footer (" + footerCounter + "," + i + ")";
+
+ // Lets use some different cell types
+ if (i % 3 == 0) {
+ row.getCell(grid.getColumn(i)).setText(caption);
+ } else if (i % 2 == 0) {
+ row.getCell(grid.getColumn(i))
+ .setHtml("<b>" + caption + "</b>");
+ } else {
+ row.getCell(grid.getColumn(i)).setWidget(new HTML(caption));
+ }
+ }
+ footerCounter++;
+ }
+
+ private void createHeaderMenu() {
+ final String[] menuPath = { "Component", "Header" };
+
+ addMenuCommand("Visible", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setHeaderVisible(!grid.isHeaderVisible());
+ }
+ }, menuPath);
+
+ addMenuCommand("Top", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setDefaultHeaderRow(grid.getHeaderRow(0));
+ }
+ }, "Component", "Header", "Default row");
+ addMenuCommand("Bottom", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setDefaultHeaderRow(grid.getHeaderRow(grid
+ .getHeaderRowCount() - 1));
+ }
+ }, "Component", "Header", "Default row");
+ addMenuCommand("Unset", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setDefaultHeaderRow(null);
+ }
+ }, "Component", "Header", "Default row");
+
+ addMenuCommand("Prepend row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureHeaderRow(grid.prependHeaderRow());
+ }
+ }, menuPath);
+ addMenuCommand("Append row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureHeaderRow(grid.appendHeaderRow());
+ }
+ }, menuPath);
+ addMenuCommand("Remove top row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.removeHeaderRow(0);
+ }
+ }, menuPath);
+ addMenuCommand("Remove bottom row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.removeHeaderRow(grid.getHeaderRowCount() - 1);
+ }
+ }, menuPath);
+
+ }
+
+ private void configureHeaderRow(final HeaderRow row) {
+ setHeaderTexts(row);
+ String rowTitle = "Row " + grid.getHeaderRowCount();
+ final String[] menuPath = { "Component", "Header", rowTitle };
+
+ addMenuCommand("Join column cells 0, 1", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(row.getCell(grid.getColumn(0)),
+ row.getCell(grid.getColumn(1))).setText(
+ "Join column cells 0, 1");
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join columns 1, 2", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumn(1), grid.getColumn(2)).setText(
+ "Join columns 1, 2");
+ ;
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join columns 3, 4, 5", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumn(3), grid.getColumn(4),
+ grid.getColumn(5)).setText("Join columns 3, 4, 5");
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join all columns", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(
+ grid.getColumns().toArray(
+ new Grid.Column[grid.getColumnCount()]))
+ .setText("Join all columns");
+ ;
+
+ }
+ }, menuPath);
+ }
+
+ private void createFooterMenu() {
+ final String[] menuPath = { "Component", "Footer" };
+
+ addMenuCommand("Visible", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setFooterVisible(!grid.isFooterVisible());
+ }
+ }, menuPath);
+
+ addMenuCommand("Prepend row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureFooterRow(grid.prependFooterRow());
+ }
+ }, menuPath);
+ addMenuCommand("Append row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureFooterRow(grid.appendFooterRow());
+ }
+ }, menuPath);
+ addMenuCommand("Remove top row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.removeFooterRow(0);
+ }
+ }, menuPath);
+ addMenuCommand("Remove bottom row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ assert grid.getFooterRowCount() > 0;
+ grid.removeFooterRow(grid.getFooterRowCount() - 1);
+ }
+ }, menuPath);
+ }
+
+ private void createEditorMenu() {
+ addMenuCommand("Enabled", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setEditorEnabled(!grid.isEditorEnabled());
+ }
+ }, "Component", "Editor");
+
+ addMenuCommand("Edit row 5", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.editRow(5);
+ }
+ }, "Component", "Editor");
+
+ addMenuCommand("Edit row 100", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.editRow(100);
+ }
+ }, "Component", "Editor");
+
+ addMenuCommand("Save", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.saveEditor();
+ }
+ }, "Component", "Editor");
+
+ addMenuCommand("Cancel edit", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.cancelEditor();
+ }
+ }, "Component", "Editor");
+
+ }
+
+ private void configureFooterRow(final FooterRow row) {
+ setFooterTexts(row);
+ String rowTitle = "Row " + grid.getFooterRowCount();
+ final String[] menuPath = { "Component", "Footer", rowTitle };
+
+ addMenuCommand("Join column cells 0, 1", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(row.getCell(grid.getColumn(0)),
+ row.getCell(grid.getColumn(1))).setText(
+ "Join column cells 0, 1");
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join columns 1, 2", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumn(1), grid.getColumn(2)).setText(
+ "Join columns 1, 2");
+ ;
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join all columns", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(
+ grid.getColumns().toArray(
+ new Grid.Column[grid.getColumnCount()]))
+ .setText("Join all columns");
+ ;
+
+ }
+ }, menuPath);
+ }
+
+ private void createDataSourceMenu() {
+ final String[] menuPath = { "Component", "DataSource" };
+
+ addMenuCommand("Reset with 100 rows of Data", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ ds.asList().clear();
+ data = createData(100);
+ ds.asList().addAll(data);
+ }
+ }, menuPath);
+
+ addMenuCommand("Reset with " + ROWS + " rows of Data",
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ ds.asList().clear();
+ data = createData(ROWS);
+ ds.asList().addAll(data);
+ }
+ }, menuPath);
+ }
+
+ /**
+ * Creates a renderer for a {@link Renderers}
+ */
+ @SuppressWarnings("rawtypes")
+ private final Renderer createRenderer(Renderers renderer) {
+ switch (renderer) {
+ case TEXT_RENDERER:
+ return new TextRenderer();
+
+ case HTML_RENDERER:
+ return new HtmlRenderer() {
+
+ @Override
+ public void render(RendererCellReference cell, String htmlString) {
+ super.render(cell, "<b>" + htmlString + "</b>");
+ }
+ };
+
+ case NUMBER_RENDERER:
+ return new NumberRenderer();
+
+ case DATE_RENDERER:
+ return new DateRenderer();
+
+ default:
+ return new TextRenderer();
+ }
+ }
+
+ /**
+ * Creates a collection of handlers for all the grid key events
+ */
+ private void createKeyHandlers() {
+ final List<VLabel> labels = new ArrayList<VLabel>();
+ for (int i = 0; i < 9; ++i) {
+ VLabel tmp = new VLabel();
+ addNorth(tmp, 20);
+ labels.add(tmp);
+ }
+
+ // Key Down Events
+ grid.addBodyKeyDownHandler(new BodyKeyDownHandler() {
+ private final VLabel label = labels.get(0);
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ grid.addHeaderKeyDownHandler(new HeaderKeyDownHandler() {
+ private final VLabel label = labels.get(1);
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ grid.addFooterKeyDownHandler(new FooterKeyDownHandler() {
+ private final VLabel label = labels.get(2);
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ // Key Up Events
+ grid.addBodyKeyUpHandler(new BodyKeyUpHandler() {
+ private final VLabel label = labels.get(3);
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ grid.addHeaderKeyUpHandler(new HeaderKeyUpHandler() {
+ private final VLabel label = labels.get(4);
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ grid.addFooterKeyUpHandler(new FooterKeyUpHandler() {
+ private final VLabel label = labels.get(5);
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ // Key Press Events
+ grid.addBodyKeyPressHandler(new BodyKeyPressHandler() {
+ private final VLabel label = labels.get(6);
+
+ @Override
+ public void onKeyPress(GridKeyPressEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ grid.addHeaderKeyPressHandler(new HeaderKeyPressHandler() {
+ private final VLabel label = labels.get(7);
+
+ @Override
+ public void onKeyPress(GridKeyPressEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ grid.addFooterKeyPressHandler(new FooterKeyPressHandler() {
+ private final VLabel label = labels.get(8);
+
+ @Override
+ public void onKeyPress(GridKeyPressEvent event) {
+ CellReference<?> focused = event.getFocusedCell();
+ updateLabel(label, event.toDebugString(),
+ focused.getRowIndex(), focused.getColumnIndex());
+ }
+ });
+
+ }
+
+ private void updateLabel(VLabel label, String output, int object, int column) {
+ String coords = "(" + object + ", " + column + ")";
+ label.setText(coords + " " + output);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java
new file mode 100644
index 0000000000..f35f9820e0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import com.google.gwt.dom.client.Document;
+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.Timer;
+import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.DateRenderer;
+import com.vaadin.client.renderers.HtmlRenderer;
+import com.vaadin.client.renderers.NumberRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.renderers.TextRenderer;
+import com.vaadin.client.renderers.WidgetRenderer;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.client.widget.grid.datasources.ListDataSource;
+import com.vaadin.client.widget.grid.datasources.ListSorter;
+import com.vaadin.client.widget.grid.sort.Sort;
+import com.vaadin.client.widget.grid.sort.SortEvent;
+import com.vaadin.client.widget.grid.sort.SortHandler;
+import com.vaadin.client.widget.grid.sort.SortOrder;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers;
+
+@Connect(GridClientColumnRenderers.GridController.class)
+public class GridClientColumnRendererConnector extends
+ AbstractComponentConnector {
+
+ public static enum Renderers {
+ TEXT_RENDERER, WIDGET_RENDERER, HTML_RENDERER, NUMBER_RENDERER, DATE_RENDERER, CPLX_RENDERER;
+ }
+
+ /**
+ * Datasource for simulating network latency
+ */
+ private class DelayedDataSource implements DataSource<String> {
+
+ private DataSource<String> ds;
+ private int firstRowIndex = -1;
+ private int numberOfRows;
+ private DataChangeHandler dataChangeHandler;
+ private int latency;
+
+ public DelayedDataSource(DataSource<String> ds, int latency) {
+ this.ds = ds;
+ this.latency = latency;
+ }
+
+ @Override
+ public void ensureAvailability(final int firstRowIndex,
+ final int numberOfRows) {
+ new Timer() {
+
+ @Override
+ public void run() {
+ DelayedDataSource.this.firstRowIndex = firstRowIndex;
+ DelayedDataSource.this.numberOfRows = numberOfRows;
+ dataChangeHandler.dataUpdated(firstRowIndex, numberOfRows);
+ dataChangeHandler
+ .dataAvailable(firstRowIndex, numberOfRows);
+ }
+ }.schedule(latency);
+ }
+
+ @Override
+ public String getRow(int rowIndex) {
+ if (rowIndex >= firstRowIndex
+ && rowIndex <= firstRowIndex + numberOfRows) {
+ return ds.getRow(rowIndex);
+ }
+ return null;
+ }
+
+ @Override
+ public int size() {
+ return ds.size();
+ }
+
+ @Override
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+ this.dataChangeHandler = dataChangeHandler;
+ }
+
+ @Override
+ public RowHandle<String> getHandle(String row) {
+ // TODO Auto-generated method stub (henrik paul: 17.6.)
+ return null;
+ }
+
+ @Override
+ public int indexOf(String row) {
+ return ds.indexOf(row);
+ }
+ }
+
+ @Override
+ protected void init() {
+ Grid<String> grid = getWidget();
+ grid.setSelectionMode(Grid.SelectionMode.NONE);
+
+ // Generated some column data
+ List<String> columnData = new ArrayList<String>();
+ for (int i = 0; i < 100; i++) {
+ columnData.add(String.valueOf(i));
+ }
+
+ // Provide data as data source
+ if (Location.getParameter("latency") != null) {
+ grid.setDataSource(new DelayedDataSource(
+ new ListDataSource<String>(columnData), Integer
+ .parseInt(Location.getParameter("latency"))));
+ } else {
+ grid.setDataSource(new ListDataSource<String>(columnData));
+ }
+
+ // Add a column to display the data in
+ Grid.Column<String, String> c = createColumnWithRenderer(Renderers.TEXT_RENDERER);
+ grid.addColumn(c);
+ grid.getDefaultHeaderRow().getCell(c).setText("Column 1");
+
+ // Add another column with a custom complex renderer
+ c = createColumnWithRenderer(Renderers.CPLX_RENDERER);
+ grid.addColumn(c);
+ grid.getDefaultHeaderRow().getCell(c).setText("Column 2");
+
+ // Add method for testing sort event firing
+ grid.addSortHandler(new SortHandler<String>() {
+ @Override
+ public void sort(SortEvent<String> event) {
+ Element console = Document.get().getElementById(
+ "testDebugConsole");
+ String text = "Client-side sort event received<br>"
+ + "Columns: " + event.getOrder().size() + ", order: ";
+ for (SortOrder order : event.getOrder()) {
+ String columnHeader = getWidget().getDefaultHeaderRow()
+ .getCell(order.getColumn()).getText();
+ text += columnHeader + ": "
+ + order.getDirection().toString();
+ }
+ console.setInnerHTML(text);
+ }
+ });
+
+ // Handle RPC calls
+ registerRpc(GridClientColumnRendererRpc.class,
+ new GridClientColumnRendererRpc() {
+
+ @Override
+ public void addColumn(Renderers renderer) {
+
+ Grid.Column<?, String> column;
+ if (renderer == Renderers.NUMBER_RENDERER) {
+ column = createNumberColumnWithRenderer(renderer);
+ } else if (renderer == Renderers.DATE_RENDERER) {
+ column = createDateColumnWithRenderer(renderer);
+ } else {
+ column = createColumnWithRenderer(renderer);
+ }
+ getWidget().addColumn(column);
+
+ getWidget()
+ .getDefaultHeaderRow()
+ .getCell(column)
+ .setText(
+ "Column "
+ + String.valueOf(getWidget()
+ .getColumnCount() + 1));
+ }
+
+ @Override
+ public void detachAttach() {
+
+ // Detach
+ HasWidgets parent = (HasWidgets) getWidget()
+ .getParent();
+ parent.remove(getWidget());
+
+ // Re-attach
+ parent.add(getWidget());
+ }
+
+ @Override
+ public void triggerClientSorting() {
+ getWidget().sort(Sort.by(getWidget().getColumn(0)));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void triggerClientSortingTest() {
+ Grid<String> grid = getWidget();
+ ListSorter<String> sorter = new ListSorter<String>(grid);
+
+ // Make sorter sort the numbers in natural order
+ sorter.setComparator(
+ (Grid.Column<String, String>) grid.getColumn(0),
+ new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ return Integer.parseInt(o1)
+ - Integer.parseInt(o2);
+ }
+ });
+
+ // Sort along column 0 in ascending order
+ grid.sort(grid.getColumn(0));
+
+ // Remove the sorter once we're done
+ sorter.removeFromGrid();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void shuffle() {
+ Grid<String> grid = getWidget();
+ ListSorter<String> shuffler = new ListSorter<String>(
+ grid);
+
+ // Make shuffler return random order
+ shuffler.setComparator(
+ (Grid.Column<String, String>) grid.getColumn(0),
+ new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ return com.google.gwt.user.client.Random
+ .nextInt(3) - 1;
+ }
+ });
+
+ // "Sort" (actually shuffle) along column 0
+ grid.sort(grid.getColumn(0));
+
+ // Remove the shuffler when we're done so that it
+ // doesn't interfere with further operations
+ shuffler.removeFromGrid();
+ }
+ });
+ }
+
+ /**
+ * Creates a a renderer for a {@link Renderers}
+ */
+ private Renderer createRenderer(Renderers renderer) {
+ switch (renderer) {
+ case TEXT_RENDERER:
+ return new TextRenderer();
+
+ case WIDGET_RENDERER:
+ return new WidgetRenderer<String, Button>() {
+
+ @Override
+ public Button createWidget() {
+ final Button button = new Button("");
+ button.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ button.setText("Clicked");
+ }
+ });
+ return button;
+ }
+
+ @Override
+ public void render(RendererCellReference cell, String data,
+ Button button) {
+ button.setHTML(data);
+ }
+ };
+
+ case HTML_RENDERER:
+ return new HtmlRenderer() {
+
+ @Override
+ public void render(RendererCellReference cell, String htmlString) {
+ super.render(cell, "<b>" + htmlString + "</b>");
+ }
+ };
+
+ case NUMBER_RENDERER:
+ return new NumberRenderer();
+
+ case DATE_RENDERER:
+ return new DateRenderer();
+
+ case CPLX_RENDERER:
+ return new ComplexRenderer<String>() {
+
+ @Override
+ public void init(RendererCellReference cell) {
+ }
+
+ @Override
+ public void render(RendererCellReference cell, String data) {
+ cell.getElement().setInnerHTML("<span>" + data + "</span>");
+ cell.getElement().getStyle().clearBackgroundColor();
+ }
+
+ @Override
+ public void setContentVisible(RendererCellReference cell,
+ boolean hasData) {
+
+ // Visualize content visible property
+ cell.getElement().getStyle()
+ .setBackgroundColor(hasData ? "green" : "red");
+
+ super.setContentVisible(cell, hasData);
+ }
+
+ @Override
+ public boolean onActivate(CellReference<?> cell) {
+ cell.getElement().setInnerHTML("<span>Activated!</span>");
+ return true;
+ }
+ };
+
+ default:
+ return new TextRenderer();
+ }
+ }
+
+ private Grid.Column<String, String> createColumnWithRenderer(
+ Renderers renderer) {
+ return new Grid.Column<String, String>(createRenderer(renderer)) {
+
+ @Override
+ public String getValue(String row) {
+ return row;
+ }
+ };
+ }
+
+ private Grid.Column<Number, String> createNumberColumnWithRenderer(
+ Renderers renderer) {
+ return new Grid.Column<Number, String>(createRenderer(renderer)) {
+
+ @Override
+ public Number getValue(String row) {
+ return Long.parseLong(row);
+ }
+ };
+ }
+
+ private Grid.Column<Date, String> createDateColumnWithRenderer(
+ Renderers renderer) {
+ return new Grid.Column<Date, String>(createRenderer(renderer)) {
+
+ @Override
+ public Date getValue(String row) {
+ return new Date();
+ }
+ };
+ }
+
+ @Override
+ public Grid<String> getWidget() {
+ return (Grid<String>) super.getWidget();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java
new file mode 100644
index 0000000000..90eee9e1c6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers;
+
+public interface GridClientColumnRendererRpc extends ClientRpc {
+
+ /**
+ * Adds a new column with a specific renderer to the grid
+ *
+ */
+ void addColumn(Renderers renderer);
+
+ /**
+ * Detaches and attaches the client side Grid
+ */
+ void detachAttach();
+
+ /**
+ * Used for client-side sorting API test
+ */
+ void triggerClientSorting();
+
+ /**
+ * @since
+ */
+ void triggerClientSortingTest();
+
+ /**
+ * @since
+ */
+ void shuffle();
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java
new file mode 100644
index 0000000000..e352b10064
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.vaadin.client.data.AbstractRemoteDataSource;
+import com.vaadin.client.renderers.TextRenderer;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.SelectionMode;
+
+public class GridClientDataSourcesWidget extends
+ PureGWTTestApplication<Grid<String[]>> {
+
+ private interface RestCallback {
+ void onResponse(RestishDataSource.Backend.Result result);
+ }
+
+ /**
+ * This is an emulated datasource that has a back-end that changes size
+ * constantly. The back-end is unable to actively push data to Grid.
+ * Instead, with each row request, in addition to its row payload it tells
+ * how many rows it contains in total.
+ *
+ * A plausible response from this REST-like api would be:
+ *
+ * <pre>
+ * <code>
+ * GET /foos/4..8
+ *
+ * {
+ * "resultsize": 4,
+ * "data": [
+ * [4, "foo IV"],
+ * [5, "foo V"],
+ * [6, "foo VI"]
+ * [7, "foo VII"]
+ * ],
+ * "totalrows": 100
+ * }
+ * </code>
+ * </pre>
+ *
+ * In this case, the size of Grid needs to be updated to be able to show 100
+ * rows in total (no more, no less).
+ *
+ * This class
+ * <ol>
+ * <li>gets initialized
+ * <li>asks for its size
+ * <li>updates Grid once the reply is received
+ * <li>as the Grid fetches more data, the total row count is dynamically
+ * updated.
+ * </ol>
+ */
+ private class RestishDataSource extends AbstractRemoteDataSource<String[]> {
+ /**
+ * Pretend like this class doesn't exist. It just simulates a backend
+ * somewhere.
+ * <p>
+ * It's scoped inside the RDS class only because it's tied to that.
+ * */
+ private class Backend {
+ public class Result {
+ public int size;
+ public List<String[]> rows;
+ }
+
+ private int size = 200;
+ private int modCount = 0;
+
+ public void query(int firstRowIndex, int numberOfRows,
+ final RestCallback callback) {
+ final Result result = new Result();
+ result.size = size;
+ result.rows = fetchRows(firstRowIndex, numberOfRows);
+
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ callback.onResponse(result);
+ }
+ });
+
+ }
+
+ private List<String[]> fetchRows(int firstRowIndex, int numberOfRows) {
+ List<String[]> rows = new ArrayList<String[]>();
+ for (int i = 0; i < numberOfRows; i++) {
+ String id = String.valueOf(firstRowIndex + i);
+ rows.add(new String[] { id, "cell " + id + " #" + modCount });
+ }
+ return rows;
+ }
+
+ public void pushRowChanges(int rows) {
+ size += rows;
+ pushRowChanges();
+ }
+
+ public void pushRowChanges() {
+ modCount++;
+
+ // push "something happened" to datasource "over the wire":
+ resetDataAndSize(size);
+ }
+
+ public void addRows(int rowcount) {
+ modCount++;
+ size += rowcount;
+ }
+ }
+
+ final Backend backend;
+
+ public RestishDataSource() {
+ backend = new Backend();
+ }
+
+ @Override
+ protected void requestRows(int firstRowIndex, int numberOfRows,
+ final RequestRowsCallback<String[]> callback) {
+
+ backend.query(firstRowIndex, numberOfRows, new RestCallback() {
+ @Override
+ public void onResponse(Backend.Result result) {
+ callback.onResponse(result.rows, result.size);
+ }
+ });
+ }
+
+ @Override
+ public Object getRowKey(String[] row) {
+ return row[0];
+ }
+ }
+
+ private final Grid<String[]> grid;
+
+ private RestishDataSource restishDataSource;
+
+ private final ScheduledCommand setRestishCommand = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ for (Grid.Column<?, String[]> column : grid.getColumns()) {
+ grid.removeColumn(column);
+ }
+
+ restishDataSource = new RestishDataSource();
+ grid.setDataSource(restishDataSource);
+ grid.addColumn(new Grid.Column<String, String[]>("column",
+ new TextRenderer()) {
+
+ @Override
+ public String getValue(String[] row) {
+ return row[1];
+ }
+ });
+ }
+ };
+
+ public GridClientDataSourcesWidget() {
+ super(new Grid<String[]>());
+ grid = getTestedWidget();
+
+ grid.getElement().getStyle().setZIndex(0);
+ grid.setHeight("400px");
+ grid.setSelectionMode(SelectionMode.NONE);
+ addNorth(grid, 400);
+
+ addMenuCommand("Use", setRestishCommand, "DataSources", "RESTish");
+ addMenuCommand("Next request +10", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ restishDataSource.backend.addRows(10);
+ }
+ }, "DataSources", "RESTish");
+ addMenuCommand("Next request -10", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ restishDataSource.backend.addRows(-10);
+ }
+ }, "DataSources", "RESTish");
+ addMenuCommand("Push data change", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ restishDataSource.backend.pushRowChanges();
+ }
+ }, "DataSources", "RESTish");
+ addMenuCommand("Push data change +10", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ restishDataSource.backend.pushRowChanges(10);
+ }
+ }, "DataSources", "RESTish");
+ addMenuCommand("Push data change -10", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ restishDataSource.backend.pushRowChanges(-10);
+ }
+ }, "DataSources", "RESTish");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridColumnAutoWidthClientWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridColumnAutoWidthClientWidget.java
new file mode 100644
index 0000000000..caaed12e70
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridColumnAutoWidthClientWidget.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.client.renderers.HtmlRenderer;
+import com.vaadin.client.widget.grid.datasources.ListDataSource;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.SelectionMode;
+
+public class GridColumnAutoWidthClientWidget extends
+ PureGWTTestApplication<Grid<List<String>>> {
+
+ private Grid<List<String>> grid;
+
+ private class Col extends Grid.Column<String, List<String>> {
+ public Col(String header) {
+ super(header, new HtmlRenderer());
+ setExpandRatio(0);
+ }
+
+ @Override
+ public String getValue(List<String> row) {
+ int index = grid.getColumns().indexOf(this);
+ return "<span>" + String.valueOf(row.get(index)) + "</span>";
+ }
+ }
+
+ public GridColumnAutoWidthClientWidget() {
+ super(new Grid<List<String>>());
+ grid = getTestedWidget();
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.setWidth("700px");
+
+ List<List<String>> list = new ArrayList<List<String>>();
+ list.add(Arrays.asList("equal length", "a very long cell content",
+ "short", "fixed width narrow", "fixed width wide"));
+ grid.setDataSource(new ListDataSource<List<String>>(list));
+
+ addColumn("equal length");
+ addColumn("short");
+ addColumn("a very long header content");
+ addColumn("fixed width narrow").setWidth(50);
+ addColumn("fixed width wide").setWidth(200);
+
+ addNorth(grid, 400);
+ }
+
+ private Col addColumn(String header) {
+ Col column = grid.addColumn(new Col(header));
+ grid.getHeaderRow(0).getCell(column)
+ .setHtml("<span>" + header + "</span>");
+ return column;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridDefaultTextRendererWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridDefaultTextRendererWidget.java
new file mode 100644
index 0000000000..4f1ea48be5
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridDefaultTextRendererWidget.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import com.vaadin.client.widget.grid.datasources.ListDataSource;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.client.widgets.Grid.SelectionMode;
+import com.vaadin.shared.ui.grid.HeightMode;
+
+public class GridDefaultTextRendererWidget extends
+ PureGWTTestApplication<Grid<String>> {
+ /*
+ * This can't be null, because grid thinks that a row object of null means
+ * "data is still being fetched".
+ */
+ private static final String NULL_STRING = "";
+
+ private Grid<String> grid;
+
+ public GridDefaultTextRendererWidget() {
+ super(new Grid<String>());
+ grid = getTestedWidget();
+
+ grid.setDataSource(new ListDataSource<String>(NULL_STRING, "string"));
+ grid.addColumn(new Column<String, String>() {
+ @Override
+ public String getValue(String row) {
+ if (!NULL_STRING.equals(row)) {
+ return row;
+ } else {
+ return null;
+ }
+ }
+ });
+
+ grid.addColumn(new Column<String, String>() {
+
+ @Override
+ public String getValue(String row) {
+ return "foo";
+ }
+
+ });
+
+ grid.setHeightByRows(2);
+ grid.setHeightMode(HeightMode.ROW);
+ grid.setSelectionMode(SelectionMode.NONE);
+ addNorth(grid, 500);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridHeightByRowOnInitWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridHeightByRowOnInitWidget.java
new file mode 100644
index 0000000000..8202c2ccc0
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridHeightByRowOnInitWidget.java
@@ -0,0 +1,32 @@
+package com.vaadin.tests.widgetset.client.grid;
+
+import java.util.Arrays;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.vaadin.client.widget.grid.datasources.ListDataSource;
+import com.vaadin.client.widgets.Grid;
+import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.shared.ui.grid.HeightMode;
+
+public class GridHeightByRowOnInitWidget extends Composite {
+ private final SimplePanel panel = new SimplePanel();
+ private final Grid<String> grid = new Grid<String>();
+
+ public GridHeightByRowOnInitWidget() {
+ initWidget(panel);
+
+ panel.setWidget(grid);
+ grid.setDataSource(new ListDataSource<String>(Arrays.asList("A", "B",
+ "C", "D", "E")));
+ grid.addColumn(new Column<String, String>("letter") {
+ @Override
+ public String getValue(String row) {
+ return row;
+ }
+ });
+
+ grid.setHeightMode(HeightMode.ROW);
+ grid.setHeightByRows(5.0d);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java
new file mode 100644
index 0000000000..e89057a148
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import com.vaadin.client.connectors.AbstractRendererConnector;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.ui.Connect;
+
+@Connect(com.vaadin.tests.components.grid.IntArrayRenderer.class)
+public class IntArrayRendererConnector extends AbstractRendererConnector<int[]> {
+
+ public static class IntArrayRenderer implements Renderer<int[]> {
+ private static final String JOINER = " :: ";
+
+ @Override
+ public void render(RendererCellReference cell, int[] data) {
+ String text = "";
+ for (int i : data) {
+ text += i + JOINER;
+ }
+ if (!text.isEmpty()) {
+ text = text.substring(0, text.length() - JOINER.length());
+ }
+ cell.getElement().setInnerText(text);
+ }
+ }
+
+ @Override
+ public IntArrayRenderer getRenderer() {
+ return (IntArrayRenderer) super.getRenderer();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java
new file mode 100644
index 0000000000..e9c126f232
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.LayoutPanel;
+import com.google.gwt.user.client.ui.MenuBar;
+import com.google.gwt.user.client.ui.Panel;
+import com.vaadin.client.ui.SubPartAware;
+
+/**
+ * Pure GWT Test Application base for testing features of a single widget;
+ * provides a menu system and convenience method for adding items to it.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class PureGWTTestApplication<T> extends DockLayoutPanel
+ implements SubPartAware {
+
+ /**
+ * Class describing a menu item with an associated action
+ */
+ public static class Command {
+ private final String title;
+ private final ScheduledCommand command;
+
+ /**
+ * Creates a Command object, which is used as an action entry in the
+ * Menu
+ *
+ * @param t
+ * a title string
+ * @param cmd
+ * a scheduled command that is executed when this item is
+ * selected
+ */
+ public Command(String t, ScheduledCommand cmd) {
+ title = t;
+ command = cmd;
+ }
+
+ /**
+ * Returns the title of this command item
+ *
+ * @return a title string
+ */
+ public final String getTitle() {
+ return title;
+ }
+
+ /**
+ * Returns the actual scheduled command of this command item
+ *
+ * @return a scheduled command
+ */
+ public final ScheduledCommand getCommand() {
+ return command;
+ }
+ }
+
+ /**
+ * A menu object, providing a complete system for building a hierarchical
+ * menu bar system.
+ */
+ public static class Menu {
+
+ private final String title;
+ private final MenuBar menubar;
+ private final List<Menu> children;
+ private final List<Command> items;
+
+ /**
+ * Create base-level menu, without a title. This is the root menu bar,
+ * which can be attached to a client application window. All other Menus
+ * should be added as child menus to this Menu, in order to maintain a
+ * nice hierarchy.
+ */
+ private Menu() {
+ title = "";
+ menubar = new MenuBar();
+ children = new ArrayList<Menu>();
+ items = new ArrayList<Command>();
+ }
+
+ /**
+ * Create a sub-menu, with a title.
+ *
+ * @param title
+ */
+ public Menu(String title) {
+ this.title = title;
+ menubar = new MenuBar(true);
+ children = new ArrayList<Menu>();
+ items = new ArrayList<Command>();
+ }
+
+ /**
+ * Return the GWT {@link MenuBar} object that provides the widget for
+ * this Menu
+ *
+ * @return a menubar object
+ */
+ public MenuBar getMenuBar() {
+ return menubar;
+ }
+
+ /**
+ * Returns the title of this menu entry
+ *
+ * @return a title string
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Adds a child menu entry to this menu. The title for this entry is
+ * taken from the Menu object argument.
+ *
+ * @param m
+ * another Menu object
+ */
+ public void addChildMenu(Menu m) {
+ menubar.addItem(m.title, m.menubar);
+ children.add(m);
+ }
+
+ /**
+ * Tests for the existence of a child menu by title at this level of the
+ * menu hierarchy
+ *
+ * @param title
+ * a title string
+ * @return true, if this menu has a direct child menu with the specified
+ * title, otherwise false
+ */
+ public boolean hasChildMenu(String title) {
+ return getChildMenu(title) != null;
+ }
+
+ /**
+ * Gets a reference to a child menu with a certain title, that is a
+ * direct child of this menu level.
+ *
+ * @param title
+ * a title string
+ * @return a Menu object with the specified title string, or null, if
+ * this menu doesn't have a direct child with the specified
+ * title.
+ */
+ public Menu getChildMenu(String title) {
+ for (Menu m : children) {
+ if (m.title.equals(title)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a command item to the menu. When the entry is clicked, the
+ * command is executed.
+ *
+ * @param cmd
+ * a command object.
+ */
+ public void addCommand(Command cmd) {
+ menubar.addItem(cmd.title, cmd.command);
+ items.add(cmd);
+ }
+
+ /**
+ * Tests for the existence of a {@link Command} that is the direct child
+ * of this level of menu.
+ *
+ * @param title
+ * the command's title
+ * @return true, if this menu level includes a command item with the
+ * specified title. Otherwise false.
+ */
+ public boolean hasCommand(String title) {
+ return getCommand(title) != null;
+ }
+
+ /**
+ * Gets a reference to a {@link Command} item that is the direct child
+ * of this level of menu.
+ *
+ * @param title
+ * the command's title
+ * @return a command, if found in this menu level, otherwise null.
+ */
+ public Command getCommand(String title) {
+ for (Command c : items) {
+ if (c.title.equals(title)) {
+ return c;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Base level menu object, provides visible menu bar
+ */
+ private final Menu menu;
+ private final T testedWidget;
+
+ /**
+ * This constructor creates the basic menu bar and adds it to the top of the
+ * parent {@link DockLayoutPanel}
+ */
+ protected PureGWTTestApplication(T widget) {
+ super(Unit.PX);
+ Panel menuPanel = new LayoutPanel();
+ menu = new Menu();
+ menuPanel.add(menu.getMenuBar());
+ addNorth(menuPanel, 25);
+ testedWidget = widget;
+ }
+
+ /**
+ * Connect an item to the menu structure
+ *
+ * @param cmd
+ * a scheduled command; see google's docs
+ * @param menupath
+ * path to the item
+ */
+ public void addMenuCommand(String title, ScheduledCommand cmd,
+ String... menupath) {
+ Menu m = createMenuPath(menupath);
+
+ m.addCommand(new Command(title, cmd));
+ }
+
+ /**
+ * Create a menu path, if one doesn't already exist, and return the last
+ * menu in the series.
+ *
+ * @param path
+ * a varargs list or array of strings describing a menu path,
+ * e.g. "File", "Recent", "User Files", which would result in the
+ * File menu having a submenu called "Recent" which would have a
+ * submenu called "User Files".
+ * @return the last Menu object specified by the path
+ */
+ private Menu createMenuPath(String... path) {
+ Menu m = menu;
+
+ for (String p : path) {
+ Menu sub = m.getChildMenu(p);
+
+ if (sub == null) {
+ sub = new Menu(p);
+ m.addChildMenu(sub);
+ }
+ m = sub;
+ }
+
+ return m;
+ }
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (testedWidget instanceof SubPartAware) {
+ return ((SubPartAware) testedWidget).getSubPartElement(subPart);
+ }
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (testedWidget instanceof SubPartAware) {
+ return ((SubPartAware) testedWidget).getSubPartName(subElement);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the tested widget.
+ *
+ * @return tested widget
+ */
+ public T getTestedWidget() {
+ return testedWidget;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java
new file mode 100644
index 0000000000..63faf1d651
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.client.grid;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.client.connectors.AbstractRendererConnector;
+import com.vaadin.client.renderers.ComplexRenderer;
+import com.vaadin.client.renderers.Renderer;
+import com.vaadin.client.widget.grid.CellReference;
+import com.vaadin.client.widget.grid.RendererCellReference;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.ui.Connect;
+
+import elemental.json.JsonObject;
+
+@Connect(com.vaadin.tests.components.grid.RowAwareRenderer.class)
+public class RowAwareRendererConnector extends AbstractRendererConnector<Void> {
+ public interface RowAwareRendererRpc extends ServerRpc {
+ void clicky(String key);
+ }
+
+ public class RowAwareRenderer extends ComplexRenderer<Void> {
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ return Arrays.asList(BrowserEvents.CLICK);
+ }
+
+ @Override
+ public void init(RendererCellReference cell) {
+ DivElement div = DivElement.as(DOM.createDiv());
+ div.setAttribute("style",
+ "border: 1px solid red; background: pink;");
+ div.setInnerText("Click me!");
+ cell.getElement().appendChild(div);
+ }
+
+ @Override
+ public void render(RendererCellReference cell, Void data) {
+ // NOOP
+ }
+
+ @Override
+ public boolean onBrowserEvent(CellReference<?> cell, NativeEvent event) {
+ String key = getRowKey((JsonObject) cell.getRow());
+ getRpcProxy(RowAwareRendererRpc.class).clicky(key);
+ cell.getElement().setInnerText(
+ "row: " + cell.getRowIndex() + ", key: " + key);
+ return true;
+ }
+ }
+
+ @Override
+ protected Renderer<Void> createRenderer() {
+ // cannot use the default createRenderer as RowAwareRenderer needs a
+ // reference to its connector - it has no "real" no-argument constructor
+ return new RowAwareRenderer();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java b/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java
new file mode 100644
index 0000000000..1bdbba2c36
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/rebind/TestWidgetRegistryGenerator.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.rebind;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+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.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.vaadin.client.metadata.Invoker;
+import com.vaadin.tests.widgetset.client.TestWidgetConnector;
+import com.vaadin.tests.widgetset.client.TestWidgetConnector.TestWidgetRegistry;
+
+public class TestWidgetRegistryGenerator extends Generator {
+
+ @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);
+ String packageName = classType.getPackage().getName();
+ String className = classType.getSimpleSourceName() + "Impl";
+
+ // Generate class source code
+ generateClass(packageName, className, logger, context);
+ return packageName + "." + className;
+ } catch (Exception e) {
+ logger.log(TreeLogger.ERROR,
+ "Accept criterion factory creation failed", e);
+ throw new UnableToCompleteException();
+ }
+ // return the fully qualifed name of the class generated
+ }
+
+ private void generateClass(String packageName, String className,
+ TreeLogger logger, GeneratorContext context) {
+ PrintWriter printWriter = context.tryCreate(logger, packageName,
+ className);
+ // print writer if null, source code has ALREADY been generated
+ if (printWriter == null) {
+ return;
+ }
+
+ // init composer, set class properties, create source writer
+ ClassSourceFileComposerFactory composer = null;
+ composer = new ClassSourceFileComposerFactory(packageName, className);
+
+ composer.setSuperclass(TestWidgetRegistry.class.getCanonicalName());
+
+ List<JClassType> testWidgets = findTestWidgets(logger,
+ context.getTypeOracle());
+
+ SourceWriter w = composer.createSourceWriter(context, printWriter);
+
+ w.println("public %s() {", className);
+ w.indent();
+
+ w.println("super();");
+ w.println();
+
+ for (JClassType testWidgetType : testWidgets) {
+ w.println("register(\"%s\", new %s() {",
+ escape(testWidgetType.getQualifiedSourceName()),
+ Invoker.class.getCanonicalName());
+ w.indent();
+
+ w.println("public Object invoke(Object target, Object... params) {");
+ w.indent();
+
+ w.println("return new %s();",
+ testWidgetType.getQualifiedSourceName());
+
+ w.outdent();
+ w.println("}");
+
+ w.outdent();
+ w.println("});");
+ w.println();
+ }
+
+ // Close constructor
+ w.outdent();
+ w.println("}");
+
+ // Close class body
+ w.outdent();
+ w.println("}");
+
+ // commit generated class
+ context.commit(logger, printWriter);
+ }
+
+ private List<JClassType> findTestWidgets(TreeLogger logger,
+ TypeOracle typeOracle) {
+ List<JClassType> testWidgetTypes = new ArrayList<JClassType>();
+
+ JClassType[] widgetTypes = typeOracle.findType(Widget.class.getName())
+ .getSubtypes();
+ for (JClassType widgetType : widgetTypes) {
+ if (isTestWidget(widgetType)) {
+ testWidgetTypes.add(widgetType);
+ }
+ }
+
+ return testWidgetTypes;
+ }
+
+ private boolean isTestWidget(JClassType widgetType) {
+ if (widgetType.isAbstract()) {
+ return false;
+ } else if (!widgetType.getPackage().getName()
+ .startsWith(TestWidgetConnector.class.getPackage().getName())) {
+ return false;
+ }
+
+ return true;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/server/LayoutDetector.java b/uitest/src/com/vaadin/tests/widgetset/server/LayoutDetector.java
new file mode 100644
index 0000000000..4b1aea67ea
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/server/LayoutDetector.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.server;
+
+import com.vaadin.tests.widgetset.client.NoLayoutRpc;
+import com.vaadin.ui.AbstractComponent;
+
+public class LayoutDetector extends AbstractComponent {
+
+ public void doNoLayoutRpc() {
+ getRpcProxy(NoLayoutRpc.class).doRpc();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/server/TestWidgetComponent.java b/uitest/src/com/vaadin/tests/widgetset/server/TestWidgetComponent.java
new file mode 100644
index 0000000000..1750e99727
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/server/TestWidgetComponent.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.server;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.TestWidgetConnector;
+import com.vaadin.tests.widgetset.client.TestWidgetConnector.TestWidgetState;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.UI;
+
+/**
+ * Testing component that shows any widget class inside the
+ * com.vaadin.tests.widgetset.client package.
+ */
+public class TestWidgetComponent extends AbstractComponent {
+ private static final String targetPackage = TestWidgetConnector.class
+ .getPackage().getName();
+
+ public TestWidgetComponent(Class<? extends Widget> widgetClass) {
+ String className = widgetClass.getCanonicalName();
+ if (!className.startsWith(targetPackage)) {
+ throw new IllegalArgumentException(
+ "Widget class must be inside the " + targetPackage
+ + " package");
+ }
+
+ getState().widgetClass = className;
+ setSizeFull();
+ }
+
+ @Override
+ public void attach() {
+ super.attach();
+
+ Class<? extends UI> uiClass = getUI().getClass();
+
+ Widgetset widgetset = uiClass.getAnnotation(Widgetset.class);
+ if (widgetset == null
+ || !widgetset.value().equals(TestingWidgetSet.NAME)) {
+ throw new IllegalStateException("You must add @"
+ + Widgetset.class.getSimpleName() + "("
+ + TestingWidgetSet.class.getSimpleName() + ".NAME) to "
+ + uiClass.getSimpleName());
+ }
+ }
+
+ @Override
+ protected TestWidgetState getState() {
+ return (TestWidgetState) super.getState();
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java
new file mode 100644
index 0000000000..ce260f108d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * 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.tests.widgetset.server.grid;
+
+import java.util.Arrays;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.label.ContentMode;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererRpc;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.AbstractSelect.ItemCaptionMode;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.CssLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.NativeButton;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.VerticalLayout;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class GridClientColumnRenderers extends UI {
+
+ /**
+ * Controls the grid on the client side
+ */
+ public static class GridController extends AbstractComponent {
+
+ private GridClientColumnRendererRpc rpc() {
+ return getRpcProxy(GridClientColumnRendererRpc.class);
+ }
+
+ /**
+ * Adds a new column with a renderer to the grid.
+ */
+ public void addColumn(Renderers renderer) {
+ rpc().addColumn(renderer);
+ }
+
+ /**
+ * Tests detaching and attaching grid
+ */
+ public void detachAttach() {
+ rpc().detachAttach();
+ }
+
+ /**
+ * @since
+ */
+ public void triggerClientSorting() {
+ rpc().triggerClientSorting();
+ }
+
+ /**
+ * @since
+ */
+ public void triggerClientSortingTest() {
+ rpc().triggerClientSortingTest();
+ }
+
+ /**
+ * @since
+ */
+ public void shuffle() {
+ rpc().shuffle();
+ }
+ }
+
+ @Override
+ protected void init(VaadinRequest request) {
+ final GridController controller = new GridController();
+ final CssLayout controls = new CssLayout();
+ final VerticalLayout content = new VerticalLayout();
+
+ content.addComponent(controller);
+ content.addComponent(controls);
+ setContent(content);
+
+ final NativeSelect select = new NativeSelect(
+ "Add Column with Renderer", Arrays.asList(Renderers.values()));
+ select.setItemCaptionMode(ItemCaptionMode.EXPLICIT);
+ for (Renderers renderer : Renderers.values()) {
+ select.setItemCaption(renderer, renderer.toString());
+ }
+ select.setValue(Renderers.TEXT_RENDERER);
+ select.setNullSelectionAllowed(false);
+ controls.addComponent(select);
+
+ NativeButton addColumnBtn = new NativeButton("Add");
+ addColumnBtn.addClickListener(new ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ Renderers renderer = (Renderers) select.getValue();
+ controller.addColumn(renderer);
+ }
+ });
+ controls.addComponent(addColumnBtn);
+
+ NativeButton detachAttachBtn = new NativeButton("DetachAttach");
+ detachAttachBtn.addClickListener(new ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.detachAttach();
+ }
+ });
+ controls.addComponent(detachAttachBtn);
+
+ NativeButton shuffleButton = new NativeButton("Shuffle");
+ shuffleButton.addClickListener(new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.shuffle();
+ }
+ });
+ controls.addComponent(shuffleButton);
+
+ NativeButton sortButton = new NativeButton("Trigger sorting event");
+ sortButton.addClickListener(new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.triggerClientSorting();
+ }
+ });
+ controls.addComponent(sortButton);
+
+ NativeButton testSortingButton = new NativeButton("Test sorting");
+ testSortingButton.addClickListener(new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.triggerClientSortingTest();
+ }
+ });
+ controls.addComponent(testSortingButton);
+
+ Label console = new Label();
+ console.setContentMode(ContentMode.HTML);
+ console.setId("testDebugConsole");
+ content.addComponent(console);
+ }
+}
diff --git a/widgets/build.xml b/widgets/build.xml
new file mode 100644
index 0000000000..5d012f4615
--- /dev/null
+++ b/widgets/build.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0"?>
+
+<project name="vaadin-widgets" basedir="." default="publish-local"
+ xmlns:ivy="antlib:org.apache.ivy.ant">
+ <description>
+ Widgets package for using Vaadin widgets with GWT 2.7+
+ </description>
+ <include file="../common.xml" as="common" />
+ <include file="../build.xml" as="vaadin" />
+ <include file="../gwt-files.xml" as="gwtfiles" />
+
+ <!-- global properties -->
+ <property name="module.name" value="vaadin-widgets" />
+ <property name="module.symbolic" value="com.vaadin.widgets" />
+ <property name="result.dir" value="result" />
+ <property name="result.src" value="${result.dir}/src" />
+ <property name="result.deps" value="${result.dir}/deps" />
+
+ <path id="classpath.compile.custom">
+ <fileset file="${gwt.user.jar}" />
+ <fileset file="${gwt.dev.jar}" />
+ </path>
+ <path id="classpath.test.custom" />
+
+ <union id="jar.includes">
+
+ </union>
+
+ <target name="dependencies">
+ <antcall target="common.dependencies" />
+ </target>
+
+ <target name="copysrc" depends="dependencies">
+ <delete dir="${result.deps}" />
+
+ <ivy:resolve transitive="false" type="jar" conf="build-provided" />
+ <ivy:cachepath pathid="vaadin.jars" />
+ <unjar dest="${result.deps}">
+ <path refid="vaadin.jars" />
+ </unjar>
+
+ <copy todir="${result.src}">
+ <fileset dir="src" />
+ <fileset dir="${result.deps}">
+ <include name="com/vaadin/*.gwt.xml" />
+ <include name="com/vaadin/client/BrowserInfo.java" />
+ <include name="com/vaadin/client/DeferredWorker.java" />
+ <include name="com/vaadin/client/Profiler.java" />
+ <include name="com/vaadin/client/StyleConstants.java" />
+ <include name="com/vaadin/client/WidgetUtil.java" />
+ <include name="com/vaadin/client/data/**/*.java" />
+ <include name="com/vaadin/client/widget/**/*.java" />
+ <include name="com/vaadin/client/Focusable.java" />
+ <include name="com/vaadin/client/widgets/*.java" />
+ <include name="com/vaadin/client/renderers/*.java" />
+ <include name="com/vaadin/client/ui/SubPartAware.java" />
+ <include name="com/vaadin/client/ui/VProgressBar.java" />
+ <include name="com/vaadin/client/VSchedulerImpl.java" />
+
+ <include name="com/vaadin/shared/ui/grid/*.java" />
+ <include name="com/vaadin/shared/ui/grid/**/*.java" />
+ <include name="com/vaadin/shared/util/SharedUtil.java" />
+ <include name="com/vaadin/shared/VBrowserDetails.java" />
+ <include
+ name="com/vaadin/shared/data/sort/SortDirection.java" />
+
+ <include name="com/vaadin/sass/linker/*.java" />
+
+ <exclude name="com/vaadin/shared/**/*Rpc.java" />
+ <exclude name="com/vaadin/shared/**/*State.java" />
+ </fileset>
+ </copy>
+
+ <mkdir dir="${result.src}/com/vaadin/themes" />
+ <copy todir="${result.src}/com/vaadin/themes/valo">
+ <fileset dir="${result.deps}/VAADIN/themes/valo" />
+ </copy>
+ <copy todir="${result.src}/com/vaadin/themes/base">
+ <fileset dir="${result.deps}/VAADIN/themes/base" />
+ </copy>
+ </target>
+ <target name="compile" description="Compiles the module"
+ depends="dependencies,copysrc">
+ <property name="classes" location="${result.dir}/classes" />
+ <mkdir dir="${classes}" />
+
+ <javac destdir="${classes}" source="${vaadin.java.version}"
+ target="${vaadin.java.version}" debug="true" encoding="UTF-8"
+ includeantruntime="false">
+ <src path="${result.src}" />
+ <classpath refid="classpath.compile.custom" />
+ <classpath refid="vaadin.jars" />
+ </javac>
+ </target>
+
+ <target name="jar" depends="compile">
+ <property name="jar.file"
+ location="${result.dir}/lib/${module.name}-${vaadin.version}.jar" />
+ <antcall target="common.jar">
+ <param name="src" value="${result.dir}/src" />
+ <reference refid="jar.includes" torefid="extra.jar.includes" />
+ </antcall>
+ </target>
+
+ <target name="publish-local" depends="jar">
+ <antcall target="common.sources.jar">
+ <reference torefid="extra.jar.includes" refid="jar.includes" />
+ </antcall>
+ <antcall target="common.javadoc.jar" />
+
+ <antcall target="common.publish-local" />
+ </target>
+
+ <target name="clean">
+ <antcall target="common.clean" />
+ </target>
+
+ <target name="checkstyle">
+ <antcall target="common.checkstyle">
+ <param name="cs.src" location="src" />
+ </antcall>
+ </target>
+
+ <target name="test" depends="checkstyle">
+ <!-- <antcall target="common.test.run" /> -->
+ </target>
+
+</project>
diff --git a/widgets/ivy.xml b/widgets/ivy.xml
new file mode 100644
index 0000000000..68aefe22b0
--- /dev/null
+++ b/widgets/ivy.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ivy-module version="2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://ant.apache.org/ivy/schemas/ivy.xsd"
+ xmlns:m="http://ant.apache.org/ivy/maven">
+
+ <info organisation="com.vaadin" module="vaadin-widgets"
+ revision="${vaadin.version}" />
+
+ <configurations>
+ <conf name="build" />
+ <conf name="build-provided" />
+ <conf name="ide" visibility="private" />
+ <conf name="test" />
+ </configurations>
+ <publications>
+ <artifact type="jar" ext="jar" />
+ <artifact type="source" ext="jar" m:classifier="sources" />
+ <artifact type="javadoc" ext="jar" m:classifier="javadoc" />
+ <artifact type="pom" ext="pom" />
+ </publications>
+ <dependencies defaultconf="build" defaultconfmapping="build->default">
+ <!-- API DEPENDENCIES -->
+
+ <!-- LIBRARY DEPENDENCIES (compile time) -->
+ <!-- Project modules -->
+ <dependency org="com.vaadin" name="vaadin-shared"
+ rev="${vaadin.version}" conf="build-provided,test->build">
+ <exclude type="pom" conf="test" />
+ </dependency>
+ <dependency org="com.vaadin" name="vaadin-client"
+ rev="${vaadin.version}" conf="build-provided,test->build">
+ <exclude type="pom" conf="test" />
+ </dependency>
+ <dependency org="com.vaadin" name="vaadin-client-compiler"
+ rev="${vaadin.version}" conf="build-provided,test->build">
+ <exclude type="pom" conf="test" />
+ </dependency>
+ <dependency org="com.vaadin" name="vaadin-themes"
+ rev="${vaadin.version}" conf="build-provided,test->build">
+ <exclude type="pom" conf="test" />
+ </dependency>
+ <dependency org="com.vaadin" name="vaadin-sass-compiler"
+ rev="${vaadin.sass.version}" conf="build-provided->default" />
+
+ </dependencies>
+
+</ivy-module>
diff --git a/widgets/src/com/vaadin/themes/Valo.gwt.xml b/widgets/src/com/vaadin/themes/Valo.gwt.xml
new file mode 100644
index 0000000000..7c58d61ecf
--- /dev/null
+++ b/widgets/src/com/vaadin/themes/Valo.gwt.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.7.0//EN"
+ "http://gwtproject.org/doctype/2.7.0/gwt-module.dtd">
+<module>
+ <entry-point class='com.vaadin.themes.valoutil.BodyStyleName' />
+ <source path='valoutil' />
+ <public path="valo" />
+ <stylesheet src="styles.css" />
+</module>
diff --git a/widgets/src/com/vaadin/themes/valoutil/BodyStyleName.java b/widgets/src/com/vaadin/themes/valoutil/BodyStyleName.java
new file mode 100644
index 0000000000..73a01b6fd2
--- /dev/null
+++ b/widgets/src/com/vaadin/themes/valoutil/BodyStyleName.java
@@ -0,0 +1,13 @@
+package com.vaadin.themes.valoutil;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.dom.client.Document;
+
+public class BodyStyleName implements EntryPoint {
+
+ @Override
+ public void onModuleLoad() {
+ Document.get().getBody().addClassName("valo");
+ }
+
+}