diff options
author | Artur Signell <artur@vaadin.com> | 2015-01-29 07:43:39 +0000 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2015-01-29 07:43:40 +0000 |
commit | a681bf8f13163b67f55ec2d334f44f976d3eb161 (patch) | |
tree | 857a4cc6ef8910db66e256da4256c21d778b6496 | |
parent | 336299a558e74e7366482640c650716880886275 (diff) | |
parent | e30b3136589799dc95049bcc87d4b7f7e707549e (diff) | |
download | vaadin-framework-a681bf8f13163b67f55ec2d334f44f976d3eb161.tar.gz vaadin-framework-a681bf8f13163b67f55ec2d334f44f976d3eb161.zip |
Merge "Merge branch 'origin/grid'"
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&ivyXmlPath=buildhelpers%2Fivy.xml&confs=ide&ivySettingsPath=%24%7Bworkspace_loc%3Avaadin%2Fivysettings.xml%7D&loadSettingsOnDemand=false&propertyFiles=build.properties"/> <classpathentry exported="true" kind="con" path="org.apache.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/?project=vaadin&ivyXmlPath=gwt%2Fivy.xml&confs=ide&ivySettingsPath=%24%7Bworkspace_loc%3Avaadin%2Fivysettings.xml%7D&loadSettingsOnDemand=false&propertyFiles=%24%7Bworkspace_loc%3Avaadin%2Fbuild.properties%7D"/> + <classpathentry kind="con" path="org.apache.ivyde.eclipse.cpcontainer.IVYDE_CONTAINER/?project=vaadin&ivyXmlPath=widgets%2Fivy.xml&confs=ide&ivySettingsPath=%24%7Bworkspace_loc%3Avaadin%2Fivysettings.xml%7D&loadSettingsOnDemand=false&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 Binary files differnew file mode 100644 index 0000000000..44ed76001a --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/grid/img/asc-light.png diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/desc-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/desc-light.png Binary files differnew file mode 100644 index 0000000000..84d15a0628 --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/grid/img/desc-light.png diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.png Binary files differnew file mode 100644 index 0000000000..20b34474c7 --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/grid/img/focus-bg-light.png 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 Binary files differnew file mode 100644 index 0000000000..4e83df03cb --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/grid/img/focus-header-bg-light.png 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 Binary files differnew file mode 100644 index 0000000000..249fd5917c --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/grid/img/focus-sel-bg-light.png diff --git a/WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.png b/WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.png Binary files differnew file mode 100644 index 0000000000..0b913e2ef1 --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/grid/img/header-bg-light.png diff --git a/WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gif b/WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gif Binary files differnew file mode 100644 index 0000000000..474b684196 --- /dev/null +++ b/WebContent/VAADIN/themes/reindeer/progressindicator/img/base-static.gif 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 Binary files differnew file mode 100644 index 0000000000..275fbc4382 --- /dev/null +++ b/WebContent/VAADIN/themes/runo/grid/img/header-bg.png diff --git a/WebContent/VAADIN/themes/runo/grid/img/resizer-bg.png b/WebContent/VAADIN/themes/runo/grid/img/resizer-bg.png Binary files differnew file mode 100644 index 0000000000..d9089775cb --- /dev/null +++ b/WebContent/VAADIN/themes/runo/grid/img/resizer-bg.png diff --git a/WebContent/VAADIN/themes/runo/grid/img/sort-asc.png b/WebContent/VAADIN/themes/runo/grid/img/sort-asc.png Binary files differnew file mode 100644 index 0000000000..44e17d5446 --- /dev/null +++ b/WebContent/VAADIN/themes/runo/grid/img/sort-asc.png diff --git a/WebContent/VAADIN/themes/runo/grid/img/sort-desc.png b/WebContent/VAADIN/themes/runo/grid/img/sort-desc.png Binary files differnew file mode 100644 index 0000000000..35fd0595f8 --- /dev/null +++ b/WebContent/VAADIN/themes/runo/grid/img/sort-desc.png diff --git a/WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gif b/WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gif Binary files differnew file mode 100644 index 0000000000..474b684196 --- /dev/null +++ b/WebContent/VAADIN/themes/runo/progressindicator/img/base-static.gif 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><h1>${title}</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><h1>${title}</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=" </dependencies>" to=" <dependency> + <replacestring from=" </dependencies>" + to=" <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.0.0.GA</version> 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 " " - escapedText = escapedText.replaceAll("<(BR|br)>", "\n"); - escapedText = escapedText.replaceAll(" ", " "); - } - 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("\"", """); - attribute = attribute.replace("'", "'"); - attribute = attribute.replace(">", ">"); - attribute = attribute.replace("<", "<"); - attribute = attribute.replace("&", "&"); - 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 " " + escapedText = escapedText.replaceAll("<(BR|br)>", "\n"); + escapedText = escapedText.replaceAll(" ", " "); + } + 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("\"", """); + attribute = attribute.replace("'", "'"); + attribute = attribute.replace(">", ">"); + attribute = attribute.replace("<", "<"); + attribute = attribute.replace("&", "&"); + 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 <T> 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 = " "; } 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 < 0 or > 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<{@link Cell}></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 < 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<Integer> ds = new ListDataSource<Integer>(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 < + * {@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]) → [0..9] + * <li>convertToVisual([15..24]) → [0..4] + * <li>convertToVisual([25..29]) → [5..9] + * <li>convertToVisual([26..39]) → [6..9] + * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([0..100]) → [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>)|×(180/π) = 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>)|×(180/π) = 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 ≤ 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><h1>${module.name}</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><h1>${module.name}</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="${atmosphere.runtime.version}";visibility:=reexport" /> + <param name="require-bundle" + value="com.vaadin.external.atmosphere.runtime;bundle-version="${atmosphere.runtime.version}";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="2.4.0",javax.servlet.http;version="2.4.0",javax.validation;version="1.0.0.GA";resolution:=optional,org.jsoup;version="1.6.3",org.jsoup.parser;version="1.6.3",org.jsoup.nodes;version="1.6.3",org.jsoup.helper;version="1.6.3",org.jsoup.safety;version="1.6.3",org.json;version="0.0.20080701"" /> - <property name="server.osgi.require" value="com.vaadin.shared;bundle-version="${vaadin.version}",com.vaadin.push;bundle-version="${vaadin.version}";resolution:=optional,com.vaadin.sass-compiler;bundle-version="${vaadin.sass.version}";resolution:=optional" /> + <property name="server.osgi.import" + value="javax.servlet;version="2.4.0",javax.servlet.http;version="2.4.0",javax.validation;version="1.0.0.GA";resolution:=optional,org.jsoup;version="1.6.3",org.jsoup.parser;version="1.6.3",org.jsoup.nodes;version="1.6.3",org.jsoup.helper;version="1.6.3",org.jsoup.safety;version="1.6.3"" /> + <property name="server.osgi.require" + value="com.vaadin.shared;bundle-version="${vaadin.version}",com.vaadin.push;bundle-version="${vaadin.version}";resolution:=optional,com.vaadin.sass-compiler;bundle-version="${vaadin.sass.version}";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 ⇆ 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<String, ?> 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<String, ?> 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> + * – 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="0.0.20080701", com.google.gwt.thirdparty.guava.common.annotations;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.base;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.base.internal;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.cache;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.collect;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.eventbus;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.io;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.net;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.primitives;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.util.concurrent;version="16.0.1.vaadin1", com.google.gwt.thirdparty.streamhtmlparser;version="0.0.10.vaadin1", com.google.gwt.thirdparty.streamhtmlparser.impl;version="0.0.10.vaadin1", com.google.gwt.thirdparty.streamhtmlparser.util;version="0.0.10.vaadin1", org.w3c.flute.parser;version="1.3.0.gg2", org.w3c.flute.parser.selectors;version="1.3.0.gg2", org.w3c.flute.util;version="1.3.0.gg2"" /> + <property name="shared.osgi.import" + value="com.google.gwt.thirdparty.guava.common.annotations;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.base;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.base.internal;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.cache;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.collect;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.eventbus;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.io;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.net;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.primitives;version="16.0.1.vaadin1", com.google.gwt.thirdparty.guava.common.util.concurrent;version="16.0.1.vaadin1", com.google.gwt.thirdparty.streamhtmlparser;version="0.0.10.vaadin1", com.google.gwt.thirdparty.streamhtmlparser.impl;version="0.0.10.vaadin1", com.google.gwt.thirdparty.streamhtmlparser.util;version="0.0.10.vaadin1", org.w3c.flute.parser;version="1.3.0.gg2", org.w3c.flute.parser.selectors;version="1.3.0.gg2", org.w3c.flute.util;version="1.3.0.gg2"" /> <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 > 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 < 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 > 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> ≤ 0, the first element will be empty, and + * the second element will be this range. If <code>length</code> + * ≥ {@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 > 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"); + } + +} |