Browse Source

Merge "Merge branch 'grid-7.5'"

tags/7.5.0.beta1
Teemu Suo-Anttila 9 years ago
parent
commit
261ccc1829
63 changed files with 12173 additions and 1068 deletions
  1. 13
    0
      WebContent/VAADIN/themes/base/escalator/escalator.scss
  2. 153
    1
      WebContent/VAADIN/themes/base/grid/grid.scss
  3. 7
    0
      WebContent/VAADIN/themes/reindeer/grid/grid.scss
  4. 19
    0
      WebContent/VAADIN/themes/runo/grid/grid.scss
  5. 39
    1
      WebContent/VAADIN/themes/valo/components/_grid.scss
  6. 11
    27
      WebContent/release-notes.html
  7. 120
    0
      client/src/com/vaadin/client/WidgetUtil.java
  8. 436
    12
      client/src/com/vaadin/client/connectors/GridConnector.java
  9. 61
    5
      client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
  10. 15
    0
      client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
  11. 241
    0
      client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java
  12. 0
    2
      client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java
  13. 0
    1
      client/src/com/vaadin/client/widget/escalator/Row.java
  14. 89
    4
      client/src/com/vaadin/client/widget/escalator/RowContainer.java
  15. 47
    0
      client/src/com/vaadin/client/widget/escalator/Spacer.java
  16. 64
    0
      client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java
  17. 689
    0
      client/src/com/vaadin/client/widget/grid/AutoScroller.java
  18. 25
    2
      client/src/com/vaadin/client/widget/grid/CellReference.java
  19. 46
    0
      client/src/com/vaadin/client/widget/grid/DetailsGenerator.java
  20. 6
    2
      client/src/com/vaadin/client/widget/grid/EventCellReference.java
  21. 6
    2
      client/src/com/vaadin/client/widget/grid/RendererCellReference.java
  22. 51
    0
      client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java
  23. 40
    0
      client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java
  24. 93
    0
      client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java
  25. 39
    0
      client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java
  26. 1798
    375
      client/src/com/vaadin/client/widgets/Escalator.java
  27. 1617
    219
      client/src/com/vaadin/client/widgets/Grid.java
  28. 377
    1
      server/src/com/vaadin/data/RpcDataProviderExtension.java
  29. 567
    9
      server/src/com/vaadin/ui/Grid.java
  30. 8
    0
      server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java
  31. 2
    1
      server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java
  32. 6
    1
      server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java
  33. 190
    0
      shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java
  34. 18
    1
      shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
  35. 9
    0
      shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
  36. 44
    0
      shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
  37. 14
    0
      shared/src/com/vaadin/shared/ui/grid/GridState.java
  38. 0
    367
      uitest/src/com/vaadin/testbench/elements/GridElement.java
  39. 121
    0
      uitest/src/com/vaadin/tests/components/grid/GridDetailsLocation.java
  40. 361
    0
      uitest/src/com/vaadin/tests/components/grid/GridDetailsLocationTest.java
  41. 67
    8
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java
  42. 19
    3
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java
  43. 282
    5
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
  44. 136
    3
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java
  45. 994
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java
  46. 649
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java
  47. 24
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSidebarFeatures.java
  48. 241
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java
  49. 156
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridSidebarContentTest.java
  50. 46
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/element/CustomGridElement.java
  51. 2
    4
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java
  52. 583
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java
  53. 347
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java
  54. 280
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnVisibilityTest.java
  55. 306
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java
  56. 84
    0
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSidebarThemeTest.java
  57. 0
    7
      uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java
  58. 20
    0
      uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java
  59. 5
    1
      uitest/src/com/vaadin/tests/util/PersonContainer.java
  60. 114
    0
      uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java
  61. 32
    3
      uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
  62. 343
    1
      uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java
  63. 1
    0
      widgets/build.xml

+ 13
- 0
WebContent/VAADIN/themes/base/escalator/escalator.scss View File

@@ -133,4 +133,17 @@
z-index: 1;
}

.#{$primaryStyleName}-spacer {
position: absolute;
display: block;

background-color: $background-color;

> td {
width: 100%;
height: 100%;
@include box-sizing(border-box);
}
}
}

+ 153
- 1
WebContent/VAADIN/themes/base/grid/grid.scss View File

@@ -1,4 +1,5 @@
$v-grid-border: 1px solid #ddd !default;
$v-grid-border-size: 1px !default;
$v-grid-border: $v-grid-border-size 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;
@@ -14,6 +15,7 @@ $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-header-drag-marked-color: $v-grid-row-selected-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;
@@ -23,6 +25,12 @@ $v-grid-cell-padding-horizontal: 5px !default;

$v-grid-editor-background-color: $v-grid-row-background-color !default;

$v-grid-details-marker-width: 2px !default;
$v-grid-details-marker-color: $v-grid-row-selected-background-color !default;
$v-grid-details-border-top: $v-grid-cell-horizontal-border !default;
$v-grid-details-border-top-stripe: $v-grid-cell-horizontal-border !default;
$v-grid-details-border-bottom: 1px solid darken($v-grid-row-stripe-background-color, 10%) !default;
$v-grid-details-border-bottom-stripe: 1px solid darken($v-grid-row-background-color, 10%) !default;

@import "../escalator/escalator";

@@ -51,6 +59,103 @@ $v-grid-editor-background-color: $v-grid-row-background-color !default;
.#{$primaryStyleName}-tablewrapper {
border: $v-grid-border;
}
// Column drag and drop elements

.#{$primaryStyleName} .header-drag-table {
border-spacing: 0;
position: relative;
table-layout: fixed;
width: inherit; // a decent default fallback

.#{$primaryStyleName}-header {
position: absolute;
> .#{$primaryStyleName}-cell {
border: $v-grid-border;
margin-top: -10px;
opacity: 0.9;
filter: alpha(opacity=90); // IE8
z-index: 30000;
}

> .#{$primaryStyleName}-drop-marker {
background-color: $v-grid-header-drag-marked-color;
position: absolute;
width: 3px;
}
}
}

// Sidebar

.#{$primaryStyleName}-sidebar.v-contextmenu {
@include box-shadow(none);
border-radius: 0;
position: absolute;
top: 0;
right: 0;

background-color: $v-grid-header-background-color;
border: $v-grid-header-border;
padding: 0;
z-index: 5;

.#{$primaryStyleName}-sidebar-button {
background: transparent;
border: none;
color: inherit;
cursor: pointer;
outline: none;
padding: 0 4px;
text-align: right;

&::-moz-focus-inner {
border: 0;
}

&:after {
content: "\f0c9";
display: block;
font-family: FontAwesome, sans-serif;
font-size: $v-grid-header-font-size;
}
}

&.closed {
border-radius: 0;
}

&.opened {
.#{$primaryStyleName}-sidebar-button {
width: 100%;
&:after {
content: "\00d7";
font-size: 16px;
line-height: 1;
}
}
}

.v-ie &.opened .#{$primaryStyleName}-sidebar-button {
vertical-align: middle;
}

.v-ie8 &.opened .#{$primaryStyleName}-sidebar-button:after {
display: inline;
}

.#{$primaryStyleName}-sidebar-content {
border-top: $v-grid-border;
padding: 4px 0;

.gwt-MenuBar {
.gwt-MenuItem .column-hiding-toggle {
text-shadow: none;
}
}
}
}

// Common cell styles

@@ -331,6 +436,53 @@ $v-grid-editor-background-color: $v-grid-row-background-color !default;
margin-right: 4px;
}

.#{$primaryStyleName}-spacer {
left: $v-grid-details-marker-width - $v-grid-border-size;
}
.#{$primaryStyleName}-spacer > td {
display: block;
padding: 0;

background-color: $v-grid-row-background-color;
border-top: $v-grid-details-border-top;
border-bottom: $v-grid-details-border-bottom;
}

.#{$primaryStyleName}-spacer.stripe > td {
background-color: $v-grid-row-stripe-background-color;
border-top: $v-grid-details-border-top-stripe;
border-bottom: $v-grid-details-border-bottom-stripe;
}
.#{$primaryStyleName}-spacer-deco-container {
border-top: $v-grid-border-size solid transparent; // same size as table wrapper border
position: relative;
top: 0; // escalator will override top for scrolling and margin-top for header offset.
z-index: 5;
}

.#{$primaryStyleName}-spacer-deco {
top: 0; // this will be overridden by code, but it's a good default.
left: 0;
width: $v-grid-details-marker-width;
background-color: $v-grid-details-marker-color;
position: absolute;
height: 100%; // this will be overridden by code, but it's a good default.
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==);
}
}

// Renderers

.#{$primaryStyleName}-cell > .v-progressbar {

+ 7
- 0
WebContent/VAADIN/themes/reindeer/grid/grid.scss View File

@@ -35,6 +35,13 @@
}
}

// Sidebar
.#{$primaryStyleName}-sidebar.v-contextmenu {
.#{$primaryStyleName}-sidebar-content {
background-color: #f8f8f9;
}
}

// Sort indicators
.#{$primaryStyleName} th.sort-asc,
.#{$primaryStyleName} th.sort-desc {

+ 19
- 0
WebContent/VAADIN/themes/runo/grid/grid.scss View File

@@ -27,6 +27,25 @@
border-color: lighten($v-grid-row-selected-background-color, 20%);
}
}
// Sidebar
.#{$primaryStyleName}-sidebar.v-contextmenu {
&.opened {
.#{$primaryStyleName}-sidebar-button {
&:after {
font-size: 22px;
}
}
}
.#{$primaryStyleName}-sidebar-content {
background-color: transparent;
.gwt-MenuBar {
border: none;
}
}
}

// Sort indicators
.#{$primaryStyleName} th.sort-asc,

+ 39
- 1
WebContent/VAADIN/themes/valo/components/_grid.scss View File

@@ -3,7 +3,8 @@
$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: flatten-list(valo-border($color: $v-grid-row-background-color, $strength: 0.8)) !default;
$v-grid-border-color-source: $v-grid-row-background-color !default;
$v-grid-border: flatten-list(valo-border($color: $v-grid-border-color-source, $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;
@@ -16,6 +17,12 @@ $v-grid-cell-padding-horizontal: $v-table-cell-padding-horizontal !default;

$v-grid-animations-enabled: $v-animations-enabled !default;

$v-grid-details-marker-width: first-number($v-grid-border) * 2 !default;
$v-grid-details-marker-color: $v-selection-color !default;
$v-grid-details-border-top: valo-border($color: $v-grid-border-color-source, $strength: 0.3) !default;
$v-grid-details-border-top-stripe: valo-border($color: $v-grid-row-stripe-background-color, $strength: 0.3) !default;
$v-grid-details-border-bottom: $v-grid-cell-horizontal-border !default;
$v-grid-details-border-bottom-stripe: $v-grid-cell-horizontal-border !default;

@import "../../base/grid/grid";

@@ -40,6 +47,15 @@ $v-grid-animations-enabled: $v-animations-enabled !default;
text-shadow: valo-text-shadow($font-color: valo-font-color($v-grid-header-background-color), $background-color: $v-grid-header-background-color);
}

.#{$primary-stylename}-header .#{$primary-stylename}-cell.dragged {
@include opacity(0.5, false);
@include transition (opacity .3s ease-in-out);
}

.#{$primary-stylename}-header .#{$primary-stylename}-cell.dragged-column-header {
margin-top: round($v-grid-row-height/-2);
}

.#{$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);
@@ -166,6 +182,28 @@ $v-grid-animations-enabled: $v-animations-enabled !default;
padding: round($v-layout-spacing-vertical / 2) round($v-layout-spacing-horizontal / 2);
margin: 0;
outline: none;
}

.#{$primary-stylename}-spacer {
margin-top: first-number($v-grid-border) * -1;
}

// Sidebar
.#{$primary-stylename}-sidebar.v-contextmenu {
&.opened {
.#{$primary-stylename}-sidebar-button:after {
font-size: 20px;
}

.#{$primary-stylename}-sidebar-content {
margin: 0 0 2px;
padding: 4px 4px 2px;
}
}

&.closed {
@include valo-gradient($v-grid-header-background-color);
}
}

// Customize scrollbars

+ 11
- 27
WebContent/release-notes.html View File

@@ -63,8 +63,10 @@
<h2 id="overview">Overview of Vaadin @version@ Release</h2>

<p>
Vaadin @version@ is a feature release that includes a
number of new features and bug fixes, as listed in the <a
Vaadin @version@ is a
<!-- feature release that includes -->
pre-release for evaluating
a number of new features and bug fixes, as listed in the <a
href="#enhancements">list of enhancements</a> and <a
href="#changelog">change log</a> below.
</p>
@@ -93,34 +95,15 @@
enhancements. Below is a list of the most notable changes:</p>

<ul>
<li>Grid is a new component for showing tabular data. It has been
designed from the ground up to eventually replace the Table
and TreeTable components.<br />
The most notable Grid features in @version-minor@ are:
<ul>
<li>Support for multiple rows in the header and footer sections.</li>
<li>Renderer concept for customizing how the data in a given column is represented in the browser.</li>
<li>Support for frozen columns.</li>
<li>Support for inline editing of one row at a time.</li>
<li>Support for components in header and footer cells.</li>
<li>Hardware accelerated, touch optimized scrolling.</li>
</ul></li>
<li>Declarative layout support for initializing a component hierarchy from an HTML file.</li>
<li>Uses GWT 2.7 for improved compilation times when using Super Dev Mode.</li>
<li>@Viewport annotation for declaratively defining a mobile viewport definition for a UI.</li>
<li>Component captions, TabSheet/Accordion tab captions and Calendar event captions can be configured to be displayed as HTML.</li>
<li>Selects use converters when presenting itemids.</li>
<li>Improved performance when server response contains no visual changes (e.g. empty polling responses).</li>
<li>Development time on-the-fly scss compilation cache may now be preserved when redeploying or restarting the server.</li>
<li>Unified JSON library for using the same API in both server-side and client-side code.</li>
<li>Range validators and converters for additional numerical types.</li>
<li>Support for fine grained add/remove item events in in-memory containers.</li>
<li>Column reordering using drag and drop in Grid</li>
<li>Column hiding in Grid</li>
<li>Row details support in Grid</li>
</ul>

<p>
For enhancements introduced in Vaadin 7.3, see the <a
href="http://vaadin.com/download/release/7.3/7.3.0/release-notes.html">Release
Notes for Vaadin 7.3.0</a>.
For enhancements introduced in Vaadin 7.4, see the <a
href="http://vaadin.com/download/release/7.4/7.4.0/release-notes.html">Release
Notes for Vaadin 7.4.0</a>.
</p>

<h3 id="incompatible">Incompatible or Behavior-altering Changes in @version-minor@</h3>
@@ -129,6 +112,7 @@
</ul>
<h3 id="knownissues">Known Issues and Limitations</h3>
<ul>
<li>The user interface for hiding and unhiding Grid columns is not yet finalized.</li>
<li>Drag'n'drop in a Table doesn't work on touch devices running
Internet Explorer (Windows Phone, Surface)
(<a href="http://dev.vaadin.com/ticket/13737">#13737</a>)

+ 120
- 0
client/src/com/vaadin/client/WidgetUtil.java View File

@@ -1459,4 +1459,124 @@ public class WidgetUtil {
return Logger.getLogger(WidgetUtil.class.getName());
}

/**
* Returns the thickness of the given element's top border.
* <p>
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since
* @param element
* the element to measure
* @return the top border thickness
*/
public static double getBorderTopThickness(Element element) {
return getBorderThickness(element, new String[] { "borderTopWidth" });
}

/**
* Returns the thickness of the given element's bottom border.
* <p>
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since
* @param element
* the element to measure
* @return the bottom border thickness
*/
public static double getBorderBottomThickness(Element element) {
return getBorderThickness(element, new String[] { "borderBottomWidth" });
}

/**
* Returns the combined thickness of the given element's top and bottom
* borders.
* <p>
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since
* @param element
* the element to measure
* @return the top and bottom border thickness
*/
public static double getBorderTopAndBottomThickness(Element element) {
return getBorderThickness(element, new String[] { "borderTopWidth",
"borderBottomWidth" });
}

/**
* Returns the thickness of the given element's left border.
* <p>
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since
* @param element
* the element to measure
* @return the left border thickness
*/
public static double getBorderLeftThickness(Element element) {
return getBorderThickness(element, new String[] { "borderLeftWidth" });
}

/**
* Returns the thickness of the given element's right border.
* <p>
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since
* @param element
* the element to measure
* @return the right border thickness
*/
public static double getBorderRightThickness(Element element) {
return getBorderThickness(element, new String[] { "borderRightWidth" });
}

/**
* Returns the thickness of the given element's left and right borders.
* <p>
* The value is determined using computed style when available and
* calculated otherwise.
*
* @since
* @param element
* the element to measure
* @return the top border thickness
*/
public static double getBorderLeftAndRightThickness(Element element) {
return getBorderThickness(element, new String[] { "borderLeftWidth",
"borderRightWidth" });
}

private static native double getBorderThickness(
com.google.gwt.dom.client.Element element, String[] borderNames)
/*-{
if (typeof $wnd.getComputedStyle === 'function') {
var computedStyle = $wnd.getComputedStyle(element);
var width = 0;
for (i=0; i< borderNames.length; i++) {
var borderWidth = computedStyle[borderNames[i]];
width += parseFloat(borderWidth);
}
return width;
} else {
var parentElement = element.offsetParent;
var cloneElement = element.cloneNode(false);
cloneElement.style.boxSizing ="content-box";
parentElement.appendChild(cloneElement);
cloneElement.style.height = "10px"; // IE8 wants the height to be set to something...
var heightWithBorder = cloneElement.offsetHeight;
for (i=0; i< borderNames.length; i++) {
cloneElement.style[borderNames[i]] = "0";
}
var heightWithoutBorder = cloneElement.offsetHeight;
parentElement.removeChild(cloneElement);
return heightWithBorder - heightWithoutBorder;
}
}-*/;
}

+ 436
- 12
client/src/com/vaadin/client/connectors/GridConnector.java View File

@@ -19,6 +19,7 @@ package com.vaadin.client.connectors;
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;
@@ -31,12 +32,15 @@ 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.Timer;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
import com.vaadin.client.DeferredWorker;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.connectors.RpcDataSourceConnector.DetailsListener;
import com.vaadin.client.connectors.RpcDataSourceConnector.RpcDataSource;
import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.renderers.Renderer;
@@ -45,11 +49,16 @@ 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.DetailsGenerator;
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.ColumnReorderEvent;
import com.vaadin.client.widget.grid.events.ColumnReorderHandler;
import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent;
import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler;
import com.vaadin.client.widget.grid.events.GridClickEvent;
import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
import com.vaadin.client.widget.grid.events.SelectAllEvent;
@@ -70,8 +79,10 @@ 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.Connector;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.grid.DetailsConnectorChange;
import com.vaadin.shared.ui.grid.EditorClientRpc;
import com.vaadin.shared.ui.grid.EditorServerRpc;
import com.vaadin.shared.ui.grid.GridClientRpc;
@@ -101,7 +112,7 @@ import elemental.json.JsonValue;
*/
@Connect(com.vaadin.ui.Grid.class)
public class GridConnector extends AbstractHasComponentsConnector implements
SimpleManagedLayout {
SimpleManagedLayout, DeferredWorker {

private static final class CustomCellStyleGenerator implements
CellStyleGenerator<JsonObject> {
@@ -167,7 +178,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements

/**
* Sets a new renderer for this column object
*
*
* @param rendererConnector
* a renderer connector object
*/
@@ -362,6 +373,305 @@ public class GridConnector extends AbstractHasComponentsConnector implements
}
}

private ColumnReorderHandler<JsonObject> columnReorderHandler = new ColumnReorderHandler<JsonObject>() {

@Override
public void onColumnReorder(ColumnReorderEvent<JsonObject> event) {
if (!columnsUpdatedFromState) {
List<Column<?, JsonObject>> columns = getWidget().getColumns();
final List<String> newColumnOrder = new ArrayList<String>();
for (Column<?, JsonObject> column : columns) {
if (column instanceof CustomGridColumn) {
newColumnOrder.add(((CustomGridColumn) column).id);
} // the other case would be the multi selection column
}
getRpcProxy(GridServerRpc.class).columnsReordered(
newColumnOrder, columnOrder);
columnOrder = newColumnOrder;
getState().columnOrder = newColumnOrder;
}
}
};

private ColumnVisibilityChangeHandler<JsonObject> columnVisibilityChangeHandler = new ColumnVisibilityChangeHandler<JsonObject>() {

@Override
public void onVisibilityChange(
ColumnVisibilityChangeEvent<JsonObject> event) {
if (!columnsUpdatedFromState) {
Column<?, JsonObject> column = event.getColumn();
if (column instanceof CustomGridColumn) {
getRpcProxy(GridServerRpc.class).columnVisibilityChanged(
((CustomGridColumn) column).id, column.isHidden(),
event.isUserOriginated());
for (GridColumnState state : getState().columns) {
if (state.id.equals(((CustomGridColumn) column).id)) {
state.hidden = event.isHidden();
break;
}
}
} else {
getLogger().warning(
"Visibility changed for a unknown column type in Grid: "
+ column.toString() + ", type "
+ column.getClass());
}
}
}
};

private static class CustomDetailsGenerator implements DetailsGenerator {

private final Map<Integer, ComponentConnector> indexToDetailsMap = new HashMap<Integer, ComponentConnector>();

@Override
@SuppressWarnings("boxing")
public Widget getDetails(int rowIndex) {
ComponentConnector componentConnector = indexToDetailsMap
.get(rowIndex);
if (componentConnector != null) {
return componentConnector.getWidget();
} else {
return null;
}
}

public void setDetailsConnectorChanges(
Set<DetailsConnectorChange> changes) {
/*
* To avoid overwriting connectors while moving them about, we'll
* take all the affected connectors, first all remove those that are
* removed or moved, then we add back those that are moved or added.
*/

/* Remove moved/removed connectors from bookkeeping */
for (DetailsConnectorChange change : changes) {
Integer oldIndex = change.getOldIndex();
Connector removedConnector = indexToDetailsMap.remove(oldIndex);

Connector connector = change.getConnector();
assert removedConnector == null || connector == null
|| removedConnector.equals(connector) : "Index "
+ oldIndex + " points to " + removedConnector
+ " while " + connector + " was expected";
}

/* Add moved/added connectors to bookkeeping */
for (DetailsConnectorChange change : changes) {
Integer newIndex = change.getNewIndex();
ComponentConnector connector = (ComponentConnector) change
.getConnector();

if (connector != null) {
assert newIndex != null : "An existing connector has a missing new index.";

ComponentConnector prevConnector = indexToDetailsMap.put(
newIndex, connector);

assert prevConnector == null : "Connector collision at index "
+ newIndex
+ " between old "
+ prevConnector
+ " and new " + connector;
}
}
}
}

@SuppressWarnings("boxing")
private static class DetailsConnectorFetcher implements DeferredWorker {

private static final int FETCH_TIMEOUT_MS = 5000;

public interface Listener {
void fetchHasBeenScheduled(int id);

void fetchHasReturned(int id);
}

/** A flag making sure that we don't call scheduleFinally many times. */
private boolean fetcherHasBeenCalled = false;

/** A rolling counter for unique values. */
private int detailsFetchCounter = 0;

/** A collection that tracks the amount of requests currently underway. */
private Set<Integer> pendingFetches = new HashSet<Integer>(5);

private final ScheduledCommand lazyDetailsFetcher = new ScheduledCommand() {
@Override
public void execute() {
int currentFetchId = detailsFetchCounter++;
pendingFetches.add(currentFetchId);
rpc.sendDetailsComponents(currentFetchId);
fetcherHasBeenCalled = false;

if (listener != null) {
listener.fetchHasBeenScheduled(currentFetchId);
}

assert assertRequestDoesNotTimeout(currentFetchId);
}
};

private DetailsConnectorFetcher.Listener listener = null;

private final GridServerRpc rpc;

public DetailsConnectorFetcher(GridServerRpc rpc) {
assert rpc != null : "RPC was null";
this.rpc = rpc;
}

public void schedule() {
if (!fetcherHasBeenCalled) {
Scheduler.get().scheduleFinally(lazyDetailsFetcher);
fetcherHasBeenCalled = true;
}
}

public void responseReceived(int fetchId) {

if (fetchId < 0) {
/* Ignore negative fetchIds (they're pushed, not fetched) */
return;
}

boolean success = pendingFetches.remove(fetchId);
assert success : "Received a response with an unidentified fetch id";

if (listener != null) {
listener.fetchHasReturned(fetchId);
}
}

@Override
public boolean isWorkPending() {
return fetcherHasBeenCalled || !pendingFetches.isEmpty();
}

private boolean assertRequestDoesNotTimeout(final int fetchId) {
/*
* This method will not be compiled without asserts enabled. This
* only makes sure that any request does not time out.
*
* TODO Should this be an explicit check? Is it worth the overhead?
*/
new Timer() {
@Override
public void run() {
assert !pendingFetches.contains(fetchId) : "Fetch id "
+ fetchId + " timed out.";
}
}.schedule(FETCH_TIMEOUT_MS);
return true;
}

public void setListener(DetailsConnectorFetcher.Listener listener) {
// if more are needed, feel free to convert this into a collection.
this.listener = listener;
}
}

/**
* The functionality that makes sure that the scroll position is still kept
* up-to-date even if more details are being fetched lazily.
*/
private class LazyDetailsScrollAdjuster implements DeferredWorker {

private static final int SCROLL_TO_END_ID = -2;
private static final int NO_SCROLL_SCHEDULED = -1;

private class ScrollStopChecker implements DeferredWorker {
private final ScheduledCommand checkCommand = new ScheduledCommand() {
@Override
public void execute() {
isScheduled = false;
if (queuedFetches.isEmpty()) {
currentRow = NO_SCROLL_SCHEDULED;
destination = null;
}
}
};

private boolean isScheduled = false;

public void schedule() {
if (isScheduled) {
return;
}
Scheduler.get().scheduleDeferred(checkCommand);
isScheduled = true;
}

@Override
public boolean isWorkPending() {
return isScheduled;
}
}

private DetailsConnectorFetcher.Listener fetcherListener = new DetailsConnectorFetcher.Listener() {
@Override
@SuppressWarnings("boxing")
public void fetchHasBeenScheduled(int id) {
if (currentRow != NO_SCROLL_SCHEDULED) {
queuedFetches.add(id);
}
}

@Override
@SuppressWarnings("boxing")
public void fetchHasReturned(int id) {
if (currentRow == NO_SCROLL_SCHEDULED
|| queuedFetches.isEmpty()) {
return;
}

queuedFetches.remove(id);
if (currentRow == SCROLL_TO_END_ID) {
getWidget().scrollToEnd();
} else {
getWidget().scrollToRow(currentRow, destination);
}

/*
* Schedule a deferred call whether we should stop adjusting for
* scrolling.
*
* This is done deferredly just because we can't be absolutely
* certain whether this most recent scrolling won't cascade into
* further lazy details loading (perhaps deferredly).
*/
scrollStopChecker.schedule();
}
};

private int currentRow = NO_SCROLL_SCHEDULED;
private final Set<Integer> queuedFetches = new HashSet<Integer>();
private final ScrollStopChecker scrollStopChecker = new ScrollStopChecker();
private ScrollDestination destination;

public LazyDetailsScrollAdjuster() {
detailsConnectorFetcher.setListener(fetcherListener);
}

public void adjustForEnd() {
currentRow = SCROLL_TO_END_ID;
}

public void adjustFor(int row, ScrollDestination destination) {
currentRow = row;
this.destination = destination;
}

@Override
public boolean isWorkPending() {
return currentRow != NO_SCROLL_SCHEDULED
|| !queuedFetches.isEmpty()
|| scrollStopChecker.isWorkPending();
}
}

/**
* Maps a generated column id to a grid column instance
*/
@@ -372,13 +682,22 @@ public class GridConnector extends AbstractHasComponentsConnector implements
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.
* {@link #selectionUpdatedFromState} 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 selectionUpdatedFromState;

/**
* {@link #columnsUpdatedFromState} is set to true when
* {@link #updateColumnOrderFromState(List)} is updating the column order
* for the widget. This flag tells the {@link #columnReorderHandler} to not
* send same data straight back to server. After updates, listener sets the
* value back to false.
*/
private boolean updatedFromState = false;
private boolean columnsUpdatedFromState;

private RpcDataSource dataSource;

@@ -388,7 +707,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements
if (event.isBatchedSelection()) {
return;
}
if (!updatedFromState) {
if (!selectionUpdatedFromState) {
for (JsonObject row : event.getRemoved()) {
selectedKeys.remove(dataSource.getRowKey(row));
}
@@ -400,7 +719,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements
getRpcProxy(GridServerRpc.class).select(
new ArrayList<String>(selectedKeys));
} else {
updatedFromState = false;
selectionUpdatedFromState = false;
}
}
};
@@ -409,6 +728,35 @@ public class GridConnector extends AbstractHasComponentsConnector implements

private String lastKnownTheme = null;

private final CustomDetailsGenerator customDetailsGenerator = new CustomDetailsGenerator();

private final DetailsConnectorFetcher detailsConnectorFetcher = new DetailsConnectorFetcher(
getRpcProxy(GridServerRpc.class));

private final DetailsListener detailsListener = new DetailsListener() {
@Override
public void reapplyDetailsVisibility(int rowIndex, JsonObject row) {
if (hasDetailsOpen(row)) {
getWidget().setDetailsVisible(rowIndex, true);
detailsConnectorFetcher.schedule();
} else {
getWidget().setDetailsVisible(rowIndex, false);
}
}

private boolean hasDetailsOpen(JsonObject row) {
return row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
&& row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE);
}

@Override
public void closeDetails(int rowIndex) {
getWidget().setDetailsVisible(rowIndex, false);
}
};

private final LazyDetailsScrollAdjuster lazyDetailsScrollAdjuster = new LazyDetailsScrollAdjuster();

@Override
@SuppressWarnings("unchecked")
public Grid<JsonObject> getWidget() {
@@ -428,6 +776,10 @@ public class GridConnector extends AbstractHasComponentsConnector implements
registerRpc(GridClientRpc.class, new GridClientRpc() {
@Override
public void scrollToStart() {
/*
* no need for lazyDetailsScrollAdjuster, because the start is
* always 0, won't change a bit.
*/
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
@@ -438,6 +790,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements

@Override
public void scrollToEnd() {
lazyDetailsScrollAdjuster.adjustForEnd();
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
@@ -449,6 +802,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements
@Override
public void scrollToRow(final int row,
final ScrollDestination destination) {
lazyDetailsScrollAdjuster.adjustFor(row, destination);
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
@@ -461,6 +815,51 @@ public class GridConnector extends AbstractHasComponentsConnector implements
public void recalculateColumnWidths() {
getWidget().recalculateColumnWidths();
}

@Override
@SuppressWarnings("boxing")
public void setDetailsConnectorChanges(
Set<DetailsConnectorChange> connectorChanges, int fetchId) {
customDetailsGenerator
.setDetailsConnectorChanges(connectorChanges);

List<DetailsConnectorChange> removedFirst = new ArrayList<DetailsConnectorChange>(
connectorChanges);
Collections.sort(removedFirst,
DetailsConnectorChange.REMOVED_FIRST_COMPARATOR);

// refresh moved/added details rows
for (DetailsConnectorChange change : removedFirst) {
Integer oldIndex = change.getOldIndex();
Integer newIndex = change.getNewIndex();

assert oldIndex == null || oldIndex >= 0 : "Got an "
+ "invalid old index: " + oldIndex
+ " (connector: " + change.getConnector() + ")";
assert newIndex == null || newIndex >= 0 : "Got an "
+ "invalid new index: " + newIndex
+ " (connector: " + change.getConnector() + ")";

if (oldIndex != null) {
/* Close the old/removed index */
getWidget().setDetailsVisible(oldIndex, false);

if (change.isShouldStillBeVisible()) {
getWidget().setDetailsVisible(oldIndex, true);
}
}

if (newIndex != null) {
/*
* Since the component was lazy loaded, we need to
* refresh the details by toggling it.
*/
getWidget().setDetailsVisible(newIndex, false);
getWidget().setDetailsVisible(newIndex, true);
}
}
detailsConnectorFetcher.responseReceived(fetchId);
}
});

getWidget().addSelectionHandler(internalSelectionChangeHandler);
@@ -503,7 +902,12 @@ public class GridConnector extends AbstractHasComponentsConnector implements
});

getWidget().setEditorHandler(new CustomEditorHandler());
getWidget().addColumnReorderHandler(columnReorderHandler);
getWidget().addColumnVisibilityChangeHandler(
columnVisibilityChangeHandler);
getWidget().setDetailsGenerator(customDetailsGenerator);
getLayoutManager().registerDependency(this, getWidget().getElement());

layout();
}

@@ -522,7 +926,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements
if (!columnIdToColumn.containsKey(state.id)) {
addColumnFromStateChangeEvent(state);
}
updateColumnFromState(columnIdToColumn.get(state.id), state);
updateColumnFromStateChangeEvent(state);
}
}

@@ -596,7 +1000,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements
columns[i] = columnIdToColumn.get(id);
i++;
}
columnsUpdatedFromState = true;
getWidget().setColumnOrder(columns);
columnsUpdatedFromState = false;
columnOrder = stateColumnOrder;
}

@@ -732,7 +1138,10 @@ public class GridConnector extends AbstractHasComponentsConnector implements
*/
private void updateColumnFromStateChangeEvent(GridColumnState columnState) {
CustomGridColumn column = columnIdToColumn.get(columnState.id);

columnsUpdatedFromState = true;
updateColumnFromState(column, columnState);
columnsUpdatedFromState = false;
}

/**
@@ -788,6 +1197,11 @@ public class GridConnector extends AbstractHasComponentsConnector implements
column.setRenderer((AbstractRendererConnector<Object>) state.rendererConnector);

column.setSortable(state.sortable);

column.setHidden(state.hidden);
column.setHidable(state.hidable);
column.setHidingToggleCaption(state.hidingToggleCaption);

column.setEditable(state.editable);
column.setEditorConnector((AbstractFieldConnector) state.editorConnector);
}
@@ -891,7 +1305,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements
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;
selectionUpdatedFromState = true;
getWidget().fireEvent(
new SelectionEvent<JsonObject>(getWidget(),
(List<JsonObject>) null, null, false));
@@ -994,4 +1408,14 @@ public class GridConnector extends AbstractHasComponentsConnector implements
public void layout() {
getWidget().onResize();
}

@Override
public boolean isWorkPending() {
return detailsConnectorFetcher.isWorkPending()
|| lazyDetailsScrollAdjuster.isWorkPending();
}

public DetailsListener getDetailsListener() {
return detailsListener;
}
}

+ 61
- 5
client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java View File

@@ -17,6 +17,7 @@
package com.vaadin.client.connectors;

import java.util.ArrayList;
import java.util.List;

import com.vaadin.client.ServerConnector;
import com.vaadin.client.data.AbstractRemoteDataSource;
@@ -43,6 +44,36 @@ import elemental.json.JsonObject;
@Connect(com.vaadin.data.RpcDataProviderExtension.class)
public class RpcDataSourceConnector extends AbstractExtensionConnector {

/**
* A callback interface to let {@link GridConnector} know that detail
* visibilities might have changed.
*
* @since 7.5.0
* @author Vaadin Ltd
*/
interface DetailsListener {

/**
* A request to verify (and correct) the visibility for a row, given
* updated metadata.
*
* @param rowIndex
* the index of the row that should be checked
* @param row
* the row object to check visibility for
* @see GridState#JSONKEY_DETAILS_VISIBLE
*/
void reapplyDetailsVisibility(int rowIndex, JsonObject row);

/**
* Closes details for a row.
*
* @param rowIndex
* the index of the row for which to close details
*/
void closeDetails(int rowIndex);
}

public class RpcDataSource extends AbstractRemoteDataSource<JsonObject> {

protected RpcDataSource() {
@@ -56,27 +87,28 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
rows.add(rowObject);
}

dataSource.setRowData(firstRow, rows);
RpcDataSource.this.setRowData(firstRow, rows);
}

@Override
public void removeRowData(int firstRow, int count) {
dataSource.removeRowData(firstRow, count);
RpcDataSource.this.removeRowData(firstRow, count);
}

@Override
public void insertRowData(int firstRow, int count) {
dataSource.insertRowData(firstRow, count);
RpcDataSource.this.insertRowData(firstRow, count);
}

@Override
public void resetDataAndSize(int size) {
dataSource.resetDataAndSize(size);
RpcDataSource.this.resetDataAndSize(size);
}
});
}

private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class);
private DetailsListener detailsListener;

@Override
protected void requestRows(int firstRowIndex, int numberOfRows,
@@ -170,7 +202,29 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
if (!handle.isPinned()) {
rpcProxy.setPinned(key, false);
}
}

void setDetailsListener(DetailsListener detailsListener) {
this.detailsListener = detailsListener;
}

@Override
protected void setRowData(int firstRowIndex, List<JsonObject> rowData) {
super.setRowData(firstRowIndex, rowData);

/*
* Intercepting details information from the data source, rerouting
* them back to the GridConnector (as a details listener)
*/
for (int i = 0; i < rowData.size(); i++) {
detailsListener.reapplyDetailsVisibility(firstRowIndex + i,
rowData.get(i));
}
}

@Override
protected void onDropFromCache(int rowIndex) {
detailsListener.closeDetails(rowIndex);
}
}

@@ -178,6 +232,8 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {

@Override
protected void extend(ServerConnector target) {
((GridConnector) target).setDataSource(dataSource);
GridConnector gridConnector = (GridConnector) target;
dataSource.setDetailsListener(gridConnector.getDetailsListener());
gridConnector.setDataSource(dataSource);
}
}

+ 15
- 0
client/src/com/vaadin/client/data/AbstractRemoteDataSource.java View File

@@ -332,9 +332,23 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
for (int i = range.getStart(); i < range.getEnd(); i++) {
T removed = indexToRowMap.remove(Integer.valueOf(i));
keyToIndexMap.remove(getRowKey(removed));

onDropFromCache(i);
}
}

/**
* A hook that can be overridden to do something whenever a row is dropped
* from the cache.
*
* @since 7.5.0
* @param rowIndex
* the index of the dropped row
*/
protected void onDropFromCache(int rowIndex) {
// noop
}

private void handleMissingRows(Range range) {
if (range.isEmpty()) {
return;
@@ -574,6 +588,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
Profiler.leave("AbstractRemoteDataSource.insertRowData");
}

@SuppressWarnings("boxing")
private void moveRowFromIndexToIndex(int oldIndex, int newIndex) {
T row = indexToRowMap.remove(oldIndex);
if (indexToRowMap.containsKey(newIndex)) {

+ 241
- 0
client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java View File

@@ -0,0 +1,241 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.dd;

import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.RootPanel;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.widgets.Grid;

/**
* A simple event handler for elements that can be drag and dropped. Properly
* handles drag start, cancel and end. For example, used in {@link Grid} column
* header reordering.
* <p>
* The showing of the dragged element, drag hints and reacting to drop/cancel is
* delegated to {@link DragAndDropCallback} implementation.
*
* @since
* @author Vaadin Ltd
*/
public class DragAndDropHandler {

/**
* Callback interface for drag and drop.
*/
public interface DragAndDropCallback {
/**
* Called when the drag has started. The drag can be canceled by
* returning {@code false}.
*
* @param startEvent
* the original event that started the drag
* @return {@code true} if the drag is OK to start, {@code false} to
* cancel
*/
boolean onDragStart(NativeEvent startEvent);

/**
* Called on drag.
*
* @param event
* the event related to the drag
*/
void onDragUpdate(NativePreviewEvent event);

/**
* Called after the has ended on a drop or cancel.
*/
void onDragEnd();

/**
* Called when the drag has ended on a drop.
*/
void onDrop();

/**
* Called when the drag has been canceled.
*/
void onDragCancel();
}

private HandlerRegistration dragStartNativePreviewHandlerRegistration;
private HandlerRegistration dragHandlerRegistration;

private boolean dragging;

private DragAndDropCallback callback;

private final NativePreviewHandler dragHandler = new NativePreviewHandler() {

@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
if (dragging) {
final int typeInt = event.getTypeInt();
switch (typeInt) {
case Event.ONKEYDOWN:
int keyCode = event.getNativeEvent().getKeyCode();
if (keyCode == KeyCodes.KEY_ESCAPE) {
// end drag if ESC is hit
cancelDrag(event);
}
break;
case Event.ONMOUSEMOVE:
case Event.ONTOUCHMOVE:
callback.onDragUpdate(event);
// prevent text selection on IE
event.getNativeEvent().preventDefault();
break;
case Event.ONTOUCHCANCEL:
cancelDrag(event);
break;
case Event.ONTOUCHEND:
/* Avoid simulated event on drag end */
event.getNativeEvent().preventDefault();
//$FALL-THROUGH$
case Event.ONMOUSEUP:
callback.onDragUpdate(event);
callback.onDrop();
stopDrag();
event.cancel();
break;
default:
break;
}
} else {
stopDrag();
}
}

};

/**
* This method can be called to trigger drag and drop on any grid element
* that can be dragged and dropped.
*
* @param dragStartingEvent
* the drag triggering event, usually a {@link Event#ONMOUSEDOWN}
* or {@link Event#ONTOUCHSTART} event on the draggable element
*
* @param callback
* the callback that will handle actual drag and drop related
* operations
*/
public void onDragStartOnDraggableElement(
final NativeEvent dragStartingEvent,
final DragAndDropCallback callback) {
dragStartNativePreviewHandlerRegistration = Event
.addNativePreviewHandler(new NativePreviewHandler() {

private int startX = WidgetUtil
.getTouchOrMouseClientX(dragStartingEvent);
private int startY = WidgetUtil
.getTouchOrMouseClientY(dragStartingEvent);

@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
final int typeInt = event.getTypeInt();
if (typeInt == -1
&& event.getNativeEvent().getType()
.toLowerCase().contains("pointer")) {
/*
* Ignore PointerEvents since IE10 and IE11 send
* also MouseEvents for backwards compatibility.
*/
return;
}
switch (typeInt) {
case Event.ONMOUSEOVER:
case Event.ONMOUSEOUT:
// we don't care
break;
case Event.ONKEYDOWN:
case Event.ONKEYPRESS:
case Event.ONKEYUP:
case Event.ONBLUR:
case Event.ONFOCUS:
// don't cancel possible drag start
break;
case Event.ONMOUSEMOVE:
case Event.ONTOUCHMOVE:
int currentX = WidgetUtil
.getTouchOrMouseClientX(event
.getNativeEvent());
int currentY = WidgetUtil
.getTouchOrMouseClientY(event
.getNativeEvent());
if (Math.abs(startX - currentX) > 3
|| Math.abs(startY - currentY) > 3) {
removeNativePreviewHandlerRegistration();
startDrag(dragStartingEvent, event, callback);
}
break;
default:
// on any other events, clean up this preview
// listener
removeNativePreviewHandlerRegistration();
break;
}
}
});
}

private void startDrag(NativeEvent startEvent,
NativePreviewEvent triggerEvent, DragAndDropCallback callback) {
if (callback.onDragStart(startEvent)) {
dragging = true;
// just capture something to prevent text selection in IE
Event.setCapture(RootPanel.getBodyElement());
this.callback = callback;
dragHandlerRegistration = Event
.addNativePreviewHandler(dragHandler);
callback.onDragUpdate(triggerEvent);
}
}

private void stopDrag() {
dragging = false;
if (dragHandlerRegistration != null) {
dragHandlerRegistration.removeHandler();
dragHandlerRegistration = null;
}
Event.releaseCapture(RootPanel.getBodyElement());
if (callback != null) {
callback.onDragEnd();
callback = null;
}
}

private void cancelDrag(NativePreviewEvent event) {
callback.onDragCancel();
callback.onDragEnd();
stopDrag();
event.cancel();
event.getNativeEvent().preventDefault();
}

private void removeNativePreviewHandlerRegistration() {
if (dragStartNativePreviewHandlerRegistration != null) {
dragStartNativePreviewHandlerRegistration.removeHandler();
dragStartNativePreviewHandlerRegistration = null;
}
}
}

+ 0
- 2
client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java View File

@@ -16,8 +16,6 @@

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

+ 0
- 1
client/src/com/vaadin/client/widget/escalator/Row.java View File

@@ -17,7 +17,6 @@
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}.

+ 89
- 4
client/src/com/vaadin/client/widget/escalator/RowContainer.java View File

@@ -22,16 +22,101 @@ import com.google.gwt.dom.client.TableSectionElement;

/**
* A representation of the rows in each of the sections (header, body and
* footer) in an {@link Escalator}.
* footer) in an {@link com.vaadin.client.widgets.Escalator}.
*
* @since 7.4
* @author Vaadin Ltd
* @see Escalator#getHeader()
* @see Escalator#getBody()
* @see Escalator#getFooter()
* @see com.vaadin.client.widgets.Escalator#getHeader()
* @see com.vaadin.client.widgets.Escalator#getBody()
* @see com.vaadin.client.widgets.Escalator#getFooter()
* @see SpacerContainer
*/
public interface RowContainer {

/**
* The row container for the body section in an
* {@link com.vaadin.client.widgets.Escalator}.
* <p>
* The body section can contain both rows and spacers.
*
* @since 7.5.0
* @author Vaadin Ltd
* @see com.vaadin.client.widgets.Escalator#getBody()
*/
public interface BodyRowContainer extends RowContainer {

/**
* Marks a spacer and its height.
* <p>
* If a spacer is already registered with the given row index, that
* spacer will be updated with the given height.
* <p>
* <em>Note:</em> The row index for a spacer will change if rows are
* inserted or removed above the current position. Spacers will also be
* removed alongside their associated rows
*
* @param rowIndex
* the row index for the spacer to modify. The affected
* spacer is underneath the given index. Use -1 to insert a
* spacer before the first row
* @param height
* the pixel height of the spacer. If {@code height} is
* negative, the affected spacer (if exists) will be removed
* @throws IllegalArgumentException
* if {@code rowIndex} is not a valid row index
* @see #insertRows(int, int)
* @see #removeRows(int, int)
*/
void setSpacer(int rowIndex, double height)
throws IllegalArgumentException;

/**
* Sets a new spacer updater.
* <p>
* Spacers that are currently visible will be updated, i.e.
* {@link SpacerUpdater#destroy(Spacer) destroyed} with the previous
* one, and {@link SpacerUpdater#init(Spacer) initialized} with the new
* one.
*
* @param spacerUpdater
* the new spacer updater
* @throws IllegalArgumentException
* if {@code spacerUpdater} is {@code null}
*/
void setSpacerUpdater(SpacerUpdater spacerUpdater)
throws IllegalArgumentException;

/**
* Gets the spacer updater currently in use.
* <p>
* {@link SpacerUpdater#NULL} is the default.
*
* @return the spacer updater currently in use. Never <code>null</code>
*/
SpacerUpdater getSpacerUpdater();

/**
* {@inheritDoc}
* <p>
* Any spacers underneath {@code index} will be offset and "pushed"
* down. This also modifies the row index they are associated with.
*/
@Override
public void insertRows(int index, int numberOfRows)
throws IndexOutOfBoundsException, IllegalArgumentException;

/**
* {@inheritDoc}
* <p>
* Any spacers underneath {@code index} will be offset and "pulled" up.
* This also modifies the row index they are associated with. Any
* spacers in the removed range will also be closed and removed.
*/
@Override
public void removeRows(int index, int numberOfRows)
throws IndexOutOfBoundsException, IllegalArgumentException;
}

/**
* An arbitrary pixel height of a row, before any autodetection for the row
* height has been made.

+ 47
- 0
client/src/com/vaadin/client/widget/escalator/Spacer.java View File

@@ -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.widget.escalator;

import com.google.gwt.dom.client.Element;

/**
* A representation of a spacer element in a
* {@link com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer}.
*
* @since 7.5.0
* @author Vaadin Ltd
*/
public interface Spacer {

/**
* Gets the root element for the spacer content.
*
* @return the root element for the spacer content
*/
Element getElement();

/**
* Gets the decorative element for this spacer.
*/
Element getDecoElement();

/**
* Gets the row index.
*
* @return the row index.
*/
int getRow();
}

+ 64
- 0
client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java View File

@@ -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.escalator;

import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;

/**
* An interface that handles the display of content for spacers.
* <p>
* The updater is responsible for making sure all elements are properly
* constructed and cleaned up.
*
* @since 7.5.0
* @author Vaadin Ltd
* @see Spacer
* @see BodyRowContainer
*/
public interface SpacerUpdater {

/** A spacer updater that does nothing. */
public static final SpacerUpdater NULL = new SpacerUpdater() {
@Override
public void init(Spacer spacer) {
// NOOP
}

@Override
public void destroy(Spacer spacer) {
// NOOP
}
};

/**
* Called whenever a spacer should be initialized with content.
*
* @param spacer
* the spacer reference that should be initialized
*/
void init(Spacer spacer);

/**
* Called whenever a spacer should be cleaned.
* <p>
* The structure to clean up is the same that has been constructed by
* {@link #init(Spacer)}.
*
* @param spacer
* the spacer reference that should be destroyed
*/
void destroy(Spacer spacer);
}

+ 689
- 0
client/src/com/vaadin/client/widget/grid/AutoScroller.java View File

@@ -0,0 +1,689 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.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.Element;
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.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.widgets.Grid;

/**
* A class for handling automatic scrolling vertically / horizontally in the
* Grid when the cursor is close enough the edge of the body of the grid,
* depending on the scroll direction chosen.
*
* @since 7.5.0
* @author Vaadin Ltd
*/
public class AutoScroller {

/**
* Callback that notifies when the cursor is on top of a new row or column
* because of the automatic scrolling.
*/
public interface AutoScrollerCallback {

/**
* Triggered when doing automatic scrolling.
* <p>
* Because the auto scroller currently only supports scrolling in one
* axis, this method is used for both vertical and horizontal scrolling.
*
* @param scrollDiff
* the amount of pixels that have been auto scrolled since
* last call
*/
void onAutoScroll(int scrollDiff);

/**
* Triggered when the grid scroll has reached the minimum scroll
* position. Depending on the scroll axis, either scrollLeft or
* scrollTop is 0.
*/
void onAutoScrollReachedMin();

/**
* Triggered when the grid scroll has reached the max scroll position.
* Depending on the scroll axis, either scrollLeft or scrollTop is at
* its maximum value.
*/
void onAutoScrollReachedMax();
}

public enum ScrollAxis {
VERTICAL, HORIZONTAL
}

/** 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;

/** The size of the autoscroll area, both top/left and bottom/right. */
private int scrollAreaPX = 100;

/**
* 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) {
/*
* Remember: targetElement is always where touchstart started, not
* where the finger is pointing currently.
*/
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 start
* event can be passed to the start(...) method.
*/
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:
// TODO investigate if this works as desired
stop();
break;
}
}

}

/**
* This class's responsibility is to scroll the table while a pointer is
* kept in a scrolling zone.
* <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 AutoScrollingFrame 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/x-coordinate on the {@link Event#getClientY() client-y}
* or {@link Event#getClientX() client-x} from where we need to start
* scrolling towards the top/left.
*/
private int startBound = -1;

/**
* The highest y/x-coordinate on the {@link Event#getClientY() client-y}
* or {@link Event#getClientX() client-x} from where we need to
* scrolling towards the bottom.
*/
private int endBound = -1;

/**
* The area where the selection acceleration takes place. If &lt;
* {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
*/
private final int gradientArea;

/**
* The number of pixels per seconds we currently are scrolling (negative
* is towards the top/left, positive is towards the bottom/right).
*/
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 pageY (VERTICAL) / pageX (HORIZONTAL) coordinate
* depending on scrolling axis.
*/
private int scrollingAxisPageCoordinate;

/** @see #doScrollAreaChecks(int) */
private int finalStartBound;

/** @see #doScrollAreaChecks(int) */
private int finalEndBound;

private boolean scrollAreaShouldRebound = false;

public AutoScrollingFrame(final int startBound, final int endBound,
final int gradientArea) {
finalStartBound = startBound;
finalEndBound = endBound;
this.gradientArea = gradientArea;
}

@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) {
double scrollPos;
double maxScrollPos;
double newScrollPos;
if (scrollDirection == ScrollAxis.VERTICAL) {
scrollPos = grid.getScrollTop();
maxScrollPos = getMaxScrollTop();
} else {
scrollPos = grid.getScrollLeft();
maxScrollPos = getMaxScrollLeft();
}
if (intPixelsToScroll > 0 && scrollPos < maxScrollPos
|| intPixelsToScroll < 0 && scrollPos > 0) {
newScrollPos = scrollPos + intPixelsToScroll;
if (scrollDirection == ScrollAxis.VERTICAL) {
grid.setScrollTop(newScrollPos);
} else {
grid.setScrollLeft(newScrollPos);
}
callback.onAutoScroll(intPixelsToScroll);
if (newScrollPos <= 0) {
callback.onAutoScrollReachedMin();
} else if (newScrollPos >= maxScrollPos) {
callback.onAutoScrollReachedMax();
}
}
}

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 (startBound < finalStartBound) {
startBound += reboundPx;
startBound = Math.min(startBound, finalStartBound);
updateScrollSpeed(scrollingAxisPageCoordinate);
} else if (endBound > finalEndBound) {
endBound -= reboundPx;
endBound = Math.max(endBound, finalEndBound);
updateScrollSpeed(scrollingAxisPageCoordinate);
}
}

private void updateScrollSpeed(final int pointerPageCordinate) {

final double ratio;
if (pointerPageCordinate < startBound) {
final double distance = pointerPageCordinate - startBound;
ratio = Math.max(-1, distance / gradientArea);
}

else if (pointerPageCordinate > endBound) {
final double distance = pointerPageCordinate - endBound;
ratio = Math.min(1, distance / gradientArea);
}

else {
ratio = 0;
}

scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
}

public void start() {
running = true;
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) {
final int pageCordinate;
if (scrollDirection == ScrollAxis.VERTICAL) {
pageCordinate = pageY;
} else {
pageCordinate = pageX;
}
doScrollAreaChecks(pageCordinate);
updateScrollSpeed(pageCordinate);
scrollingAxisPageCoordinate = pageCordinate;
}

/**
* 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/left, otherwise below/right).
*/
private void doScrollAreaChecks(int pageCordinate) {
/*
* The first run makes sure that neither scroll position is
* underneath the finger, but offset to either direction from
* underneath the pointer.
*/
if (startBound == -1) {
startBound = Math.min(finalStartBound, pageCordinate);
endBound = Math.max(finalEndBound, pageCordinate);
}

/*
* 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 = startBound;
if (startBound < finalStartBound) {
startBound = Math.max(startBound,
Math.min(finalStartBound, pageCordinate));
}

int oldBottomBound = endBound;
if (endBound > finalEndBound) {
endBound = Math.min(endBound,
Math.max(finalEndBound, pageCordinate));
}

final boolean startDidNotMove = oldTopBound == startBound;
final boolean endDidNotMove = oldBottomBound == endBound;
final boolean wasMovement = pageCordinate != scrollingAxisPageCoordinate;
scrollAreaShouldRebound = (startDidNotMove && endDidNotMove && wasMovement);
}
}
}

/**
* This handler makes sure that pointer movements are handled.
* <p>
* Essentially, 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 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 registration info for {@link #scrollPreviewHandler} */
private HandlerRegistration handlerRegistration;

/**
* The top/left bound, as calculated from the {@link Event#getClientY()
* client-y} or {@link Event#getClientX() client-x} coordinates.
*/
private double startingBound = -1;

/**
* The bottom/right bound, as calculated from the {@link Event#getClientY()
* client-y} or or {@link Event#getClientX() client-x} coordinates.
*/
private int endingBound = -1;

/** The size of the autoscroll acceleration area. */
private int gradientArea;

private Grid<?> grid;

private HandlerRegistration nativePreviewHandlerRegistration;

private ScrollAxis scrollDirection;

private AutoScrollingFrame autoScroller;

private AutoScrollerCallback callback;

/**
* Creates a new instance for scrolling the given grid.
*
* @param grid
* the grid to auto scroll
*/
public AutoScroller(Grid<?> grid) {
this.grid = grid;
}

/**
* Starts the automatic scrolling detection.
*
* @param startEvent
* the event that starts the automatic scroll
* @param scrollAxis
* the axis along which the scrolling should happen
* @param callback
* the callback for getting info about the automatic scrolling
*/
public void start(final NativeEvent startEvent, ScrollAxis scrollAxis,
AutoScrollerCallback callback) {
scrollDirection = scrollAxis;
this.callback = callback;
injectNativeHandler();
start();
startEvent.preventDefault();
startEvent.stopPropagation();
}

/**
* Stops the automatic scrolling.
*/
public void stop() {
if (handlerRegistration != null) {
handlerRegistration.removeHandler();
handlerRegistration = null;
}

if (autoScroller != null) {
autoScroller.stop();
autoScroller = null;
}

removeNativeHandler();
}

/**
* Set the auto scroll area height or width depending on the scrolling axis.
* This is the amount of pixels from the edge of the grid that the scroll is
* triggered.
* <p>
* Defaults to 100px.
*
* @param px
* the pixel height/width for the auto scroll area depending on
* direction
*/
public void setScrollArea(int px) {
scrollAreaPX = px;
}

/**
* Returns the size of the auto scroll area in pixels.
* <p>
* Defaults to 100px.
*
* @return size in pixels
*/
public int getScrollArea() {
return scrollAreaPX;
}

private void start() {
/*
* 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 AutoScrollingFrame((int) Math.ceil(startingBound),
endingBound, gradientArea);
autoScroller.start();
}

private void updateScrollBounds() {
double startBorder = getBodyClientStart();
final int endBorder = getBodyClientEnd();
startBorder += getFrozenColumnsWidth();

final int scrollCompensation = getScrollCompensation();
startingBound = scrollCompensation + startBorder + scrollAreaPX;
endingBound = scrollCompensation + endBorder - scrollAreaPX;
gradientArea = scrollAreaPX;

// modify bounds if they're too tightly packed
if (endingBound - startingBound < MIN_NO_AUTOSCROLL_AREA_PX) {
double adjustment = MIN_NO_AUTOSCROLL_AREA_PX
- (endingBound - startingBound);
startingBound -= adjustment / 2;
endingBound += adjustment / 2;
gradientArea -= adjustment / 2;
}
}

private int getScrollCompensation() {
Element cursor = grid.getElement();
int scroll = 0;
while (cursor != null) {
scroll -= scrollDirection == ScrollAxis.VERTICAL ? cursor
.getScrollTop() : cursor.getScrollLeft();
cursor = cursor.getParentElement();
}

return scroll;
}

private void injectNativeHandler() {
removeNativeHandler();
nativePreviewHandlerRegistration = Event
.addNativePreviewHandler(new TouchEventHandler());
}

private void removeNativeHandler() {
if (nativePreviewHandlerRegistration != null) {
nativePreviewHandlerRegistration.removeHandler();
nativePreviewHandlerRegistration = null;
}
}

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;
}

/** Get the "left" of an element in relation to "client" coordinates. */
@SuppressWarnings("static-method")
private int getClientLeft(final Element e) {
Element cursor = e;
int left = 0;
while (cursor != null) {
left += cursor.getOffsetLeft();
cursor = cursor.getOffsetParent();
}
return left;
}

private int getBodyClientEnd() {
if (scrollDirection == ScrollAxis.VERTICAL) {
return getClientTop(getTfootElement()) - 1;
} else {
TableSectionElement tbodyElement = getTbodyElement();
return getClientLeft(tbodyElement) + tbodyElement.getOffsetWidth()
- 1;
}

}

private int getBodyClientStart() {
if (scrollDirection == ScrollAxis.VERTICAL) {
return getClientTop(grid.getElement())
+ getTheadElement().getOffsetHeight();
} else {
return getClientLeft(getTbodyElement());
}
}

private double getFrozenColumnsWidth() {
double value = getMultiSelectColumnWidth();
for (int i = 0; i < grid.getFrozenColumnCount(); i++) {
value += grid.getColumn(i).getWidthActual();
}
return value;
}

private double getMultiSelectColumnWidth() {
if (grid.getFrozenColumnCount() >= 0
&& grid.getSelectionModel().getSelectionColumnRenderer() != null) {
// frozen checkbox column is present
return getTheadElement().getFirstChildElement()
.getFirstChildElement().getOffsetWidth();
}
return 0.0;
}

private double getMaxScrollLeft() {
return grid.getScrollWidth()
- (getTableElement().getParentElement().getOffsetWidth() - getFrozenColumnsWidth());
}

private double getMaxScrollTop() {
return grid.getScrollHeight() - getTfootElement().getOffsetHeight()
- getTheadElement().getOffsetHeight();
}
}

+ 25
- 2
client/src/com/vaadin/client/widget/grid/CellReference.java View File

@@ -32,6 +32,8 @@ import com.vaadin.client.widgets.Grid;
* @since 7.4
*/
public class CellReference<T> {

private int columnIndexDOM;
private int columnIndex;
private Grid.Column<?, T> column;
private final RowReference<T> rowReference;
@@ -42,13 +44,20 @@ public class CellReference<T> {

/**
* Sets the identifying information for this cell.
* <p>
* The difference between {@link #columnIndexDOM} and {@link #columnIndex}
* comes from hidden columns.
*
* @param columnIndexDOM
* the index of the column in the DOM
* @param columnIndex
* the index of the column
* @param column
* the column object
*/
public void set(int columnIndex, Grid.Column<?, T> column) {
public void set(int columnIndexDOM, int columnIndex,
Grid.Column<?, T> column) {
this.columnIndexDOM = columnIndexDOM;
this.columnIndex = columnIndex;
this.column = column;
}
@@ -82,6 +91,9 @@ public class CellReference<T> {

/**
* Gets the index of the column.
* <p>
* <em>NOTE:</em> The index includes hidden columns in the count, unlike
* {@link #getColumnIndexDOM()}.
*
* @return the index of the column
*/
@@ -89,6 +101,17 @@ public class CellReference<T> {
return columnIndex;
}

/**
* Gets the index of the cell in the DOM. The difference to
* {@link #getColumnIndex()} is caused by hidden columns.
*
* @since 7.5.0
* @return the index of the column in the DOM
*/
public int getColumnIndexDOM() {
return columnIndexDOM;
}

/**
* Gets the column objects.
*
@@ -113,7 +136,7 @@ public class CellReference<T> {
* @return the element of the cell
*/
public TableCellElement getElement() {
return rowReference.getElement().getCells().getItem(columnIndex);
return rowReference.getElement().getCells().getItem(columnIndexDOM);
}

/**

+ 46
- 0
client/src/com/vaadin/client/widget/grid/DetailsGenerator.java View File

@@ -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.client.widget.grid;

import com.google.gwt.user.client.ui.Widget;

/**
* A callback interface for generating details for a particular row in Grid.
*
* @since 7.5.0
* @author Vaadin Ltd
*/
public interface DetailsGenerator {

/** A details generator that provides no details */
public static final DetailsGenerator NULL = new DetailsGenerator() {
@Override
public Widget getDetails(int rowIndex) {
return null;
}
};

/**
* This method is called for whenever a new details row needs to be
* generated.
*
* @param rowIndex
* the index of the row for which to generate details
* @return the details for the given row, or <code>null</code> to leave the
* details empty.
*/
Widget getDetails(int rowIndex);
}

+ 6
- 2
client/src/com/vaadin/client/widget/grid/EventCellReference.java View File

@@ -18,6 +18,7 @@ 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;
import com.vaadin.client.widgets.Grid.Column;

/**
* A data class which contains information which identifies a cell being the
@@ -48,11 +49,14 @@ public class EventCellReference<T> extends CellReference<T> {
*/
public void set(Cell targetCell) {
int row = targetCell.getRow();
int column = targetCell.getColumn();
int columnIndexDOM = targetCell.getColumn();
Column<?, T> column = grid.getVisibleColumns().get(columnIndexDOM);

// 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));
int columnIndex = grid.getColumns().indexOf(column);
set(columnIndexDOM, columnIndex, column);

this.element = targetCell.getElement();
}

+ 6
- 2
client/src/com/vaadin/client/widget/grid/RendererCellReference.java View File

@@ -49,12 +49,16 @@ public class RendererCellReference extends CellReference<Object> {
*
* @param cell
* the flyweight cell to reference
* @param columnIndex
* the index of the column in the grid, including hidden cells
* @param column
* the column to reference
*/
public void set(FlyweightCell cell, Grid.Column<?, ?> column) {
public void set(FlyweightCell cell, int columnIndex,
Grid.Column<?, ?> column) {
this.cell = cell;
super.set(cell.getColumn(), (Grid.Column<?, Object>) column);
super.set(cell.getColumn(), columnIndex,
(Grid.Column<?, Object>) column);
}

/**

+ 51
- 0
client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java View File

@@ -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.client.widget.grid.events;

import com.google.gwt.event.shared.GwtEvent;

/**
* An event for notifying that the columns in the Grid have been reordered.
*
* @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.5.0
* @author Vaadin Ltd
*/
public class ColumnReorderEvent<T> extends GwtEvent<ColumnReorderHandler<T>> {

/**
* Handler type.
*/
private final static Type<ColumnReorderHandler<?>> TYPE = new Type<ColumnReorderHandler<?>>();

public static final Type<ColumnReorderHandler<?>> getType() {
return TYPE;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Type<ColumnReorderHandler<T>> getAssociatedType() {
return (Type) TYPE;
}

@Override
protected void dispatch(ColumnReorderHandler<T> handler) {
handler.onColumnReorder(this);
}

}

+ 40
- 0
client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java View File

@@ -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.EventHandler;

/**
* Handler for a Grid column reorder event, called when the Grid's columns has
* been reordered.
*
* @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.5.0
* @author Vaadin Ltd
*/
public interface ColumnReorderHandler<T> extends EventHandler {

/**
* A column reorder event, fired by Grid when the columns of the Grid have
* been reordered.
*
* @param event
* column reorder event
*/
public void onColumnReorder(ColumnReorderEvent<T> event);
}

+ 93
- 0
client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java View File

@@ -0,0 +1,93 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.widgets.Grid.Column;

/**
* An event for notifying that the columns in the Grid's have changed
* visibility.
*
* @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.5.0
* @author Vaadin Ltd
*/
public class ColumnVisibilityChangeEvent<T> extends
GwtEvent<ColumnVisibilityChangeHandler<T>> {

private final static Type<ColumnVisibilityChangeHandler<?>> TYPE = new Type<ColumnVisibilityChangeHandler<?>>();

public static final Type<ColumnVisibilityChangeHandler<?>> getType() {
return TYPE;
}

private final Column<?, T> column;

private final boolean userOriginated;

private final boolean hidden;

public ColumnVisibilityChangeEvent(Column<?, T> column, boolean hidden,
boolean userOriginated) {
this.column = column;
this.hidden = hidden;
this.userOriginated = userOriginated;
}

/**
* Returns the column where the visibility change occurred.
*
* @return the column where the visibility change occurred.
*/
public Column<?, T> getColumn() {
return column;
}

/**
* Was the column set hidden or visible.
*
* @return <code>true</code> if the column was hidden <code>false</code> if
* it was set visible
*/
public boolean isHidden() {
return hidden;
}

/**
* Is the visibility change triggered by user.
*
* @return <code>true</code> if the change was triggered by user,
* <code>false</code> if not
*/
public boolean isUserOriginated() {
return userOriginated;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public com.google.gwt.event.shared.GwtEvent.Type<ColumnVisibilityChangeHandler<T>> getAssociatedType() {
return (Type) TYPE;
}

@Override
protected void dispatch(ColumnVisibilityChangeHandler<T> handler) {
handler.onVisibilityChange(this);
}

}

+ 39
- 0
client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java View File

@@ -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;

/**
* Handler for a Grid column visibility change event, called when the Grid's
* columns have changed visibility to hidden or visible.
*
* @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.5.0
* @author Vaadin Ltd
*/
public interface ColumnVisibilityChangeHandler<T> extends EventHandler {

/**
* A column visibility change event, fired by Grid when a column in the Grid
* has changed visibility.
*
* @param event
* column visibility change event
*/
public void onVisibilityChange(ColumnVisibilityChangeEvent<T> event);
}

+ 1798
- 375
client/src/com/vaadin/client/widgets/Escalator.java
File diff suppressed because it is too large
View File


+ 1617
- 219
client/src/com/vaadin/client/widgets/Grid.java
File diff suppressed because it is too large
View File


+ 377
- 1
server/src/com/vaadin/data/RpcDataProviderExtension.java View File

@@ -25,11 +25,15 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.Indexed.ItemAddEvent;
import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
@@ -45,12 +49,16 @@ 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.DetailsConnectorChange;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.ui.Component;
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.DetailsGenerator;
import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.RowStyleGenerator;
import com.vaadin.ui.renderers.Renderer;
@@ -110,11 +118,16 @@ public class RpcDataProviderExtension extends AbstractExtension {
}

for (Object itemId : itemsRemoved) {
detailComponentManager.destroyDetails(itemId);
itemIdToKey.remove(itemId);
}

for (Object itemId : itemSet) {
itemIdToKey.put(itemId, getKey(itemId));
if (visibleDetails.contains(itemId)) {
detailComponentManager.createDetails(itemId,
indexOf(itemId));
}
}
}

@@ -122,7 +135,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
return String.valueOf(rollingIndex++);
}

String getKey(Object itemId) {
public String getKey(Object itemId) {
String key = itemIdToKey.get(itemId);
if (key == null) {
key = nextKey();
@@ -571,6 +584,270 @@ public class RpcDataProviderExtension extends AbstractExtension {
}
}

/**
* A class that makes detail component related internal communication
* possible between {@link RpcDataProviderExtension} and grid.
*
* @since 7.5.0
* @author Vaadin Ltd
*/
public static final class DetailComponentManager implements Serializable {
/**
* This map represents all the components that have been requested for
* each item id.
* <p>
* Normally this map is consistent with what is displayed in the
* component hierarchy (and thus the DOM). The only time this map is out
* of sync with the DOM is between the any calls to
* {@link #createDetails(Object, int)} or
* {@link #destroyDetails(Object)}, and
* {@link GridClientRpc#setDetailsConnectorChanges(Set)}.
* <p>
* This is easily checked: if {@link #unattachedComponents} is
* {@link Collection#isEmpty() empty}, then this field is consistent
* with the connector hierarchy.
*/
private final Map<Object, Component> visibleDetailsComponents = Maps
.newHashMap();

/** A lookup map for which row contains which details component. */
private BiMap<Integer, Component> rowIndexToDetails = HashBiMap
.create();

/**
* A copy of {@link #rowIndexToDetails} from its last stable state. Used
* for creating a diff against {@link #rowIndexToDetails}.
*
* @see #getAndResetConnectorChanges()
*/
private BiMap<Integer, Component> prevRowIndexToDetails = HashBiMap
.create();

/**
* A set keeping track on components that have been created, but not
* attached. They should be attached at some later point in time.
* <p>
* This isn't strictly requried, but it's a handy explicit log. You
* could find out the same thing by taking out all the other components
* and checking whether Grid is their parent or not.
*/
private final Set<Component> unattachedComponents = Sets.newHashSet();

/**
* Keeps tabs on all the details that did not get a component during
* {@link #createDetails(Object, int)}.
*/
private final Map<Object, Integer> emptyDetails = Maps.newHashMap();

private Grid grid;

/**
* Creates a details component by the request of the client side, with
* the help of the user-defined {@link DetailsGenerator}.
* <p>
* Also keeps internal bookkeeping up to date.
*
* @param itemId
* the item id for which to create the details component.
* Assumed not <code>null</code> and that a component is not
* currently present for this item previously
* @param rowIndex
* the row index for {@code itemId}
* @throws IllegalStateException
* if the current details generator provides a component
* that was manually attached, or if the same instance has
* already been provided
*/
public void createDetails(Object itemId, int rowIndex)
throws IllegalStateException {
assert itemId != null : "itemId was null";
Integer newRowIndex = Integer.valueOf(rowIndex);

if (visibleDetailsComponents.containsKey(itemId)) {
// Don't overwrite existing components
return;
}

RowReference rowReference = new RowReference(grid);
rowReference.set(itemId);

DetailsGenerator detailsGenerator = grid.getDetailsGenerator();
Component details = detailsGenerator.getDetails(rowReference);
if (details != null) {
String generatorName = detailsGenerator.getClass().getName();
if (details.getParent() != null) {
throw new IllegalStateException(generatorName
+ " generated a details component that already "
+ "was attached. (itemId: " + itemId + ", row: "
+ rowIndex + ", component: " + details);
}

if (rowIndexToDetails.containsValue(details)) {
throw new IllegalStateException(generatorName
+ " provided a details component that already "
+ "exists in Grid. (itemId: " + itemId + ", row: "
+ rowIndex + ", component: " + details);
}

visibleDetailsComponents.put(itemId, details);
rowIndexToDetails.put(newRowIndex, details);
unattachedComponents.add(details);

assert !emptyDetails.containsKey(itemId) : "Bookeeping thinks "
+ "itemId is empty even though we just created a "
+ "component for it (" + itemId + ")";
} else {
assert assertItemIdHasNotMovedAndNothingIsOverwritten(itemId,
newRowIndex);
emptyDetails.put(itemId, newRowIndex);
}

/*
* Don't attach the components here. It's done by
* GridServerRpc.sendDetailsComponents in a separate roundtrip.
*/
}

private boolean assertItemIdHasNotMovedAndNothingIsOverwritten(
Object itemId, Integer newRowIndex) {

Integer oldRowIndex = emptyDetails.get(itemId);
if (!SharedUtil.equals(oldRowIndex, newRowIndex)) {

assert !emptyDetails.containsKey(itemId) : "Unexpected "
+ "change of empty details row index for itemId "
+ itemId + " from " + oldRowIndex + " to "
+ newRowIndex;

assert !emptyDetails.containsValue(newRowIndex) : "Bookkeeping"
+ " already had another itemId for this empty index "
+ "(index: " + newRowIndex + ", new itemId: " + itemId
+ ")";
}

return true;
}

/**
* Destroys correctly a details component, by the request of the client
* side.
* <p>
* Also keeps internal bookkeeping up to date.
*
* @param itemId
* the item id for which to destroy the details component
*/
public void destroyDetails(Object itemId) {
emptyDetails.remove(itemId);

Component removedComponent = visibleDetailsComponents
.remove(itemId);
if (removedComponent == null) {
return;
}

rowIndexToDetails.inverse().remove(removedComponent);

removedComponent.setParent(null);
grid.markAsDirty();
}

/**
* Gets all details components that are currently attached to the grid.
* <p>
* Used internally by the Grid object.
*
* @return all details components that are currently attached to the
* grid
*/
public Collection<Component> getComponents() {
Set<Component> components = new HashSet<Component>(
visibleDetailsComponents.values());
components.removeAll(unattachedComponents);
return components;
}

/**
* Gets information on how the connectors have changed.
* <p>
* This method only returns the changes that have been made between two
* calls of this method. I.e. Calling this method once will reset the
* state for the next state.
* <p>
* Used internally by the Grid object.
*
* @return information on how the connectors have changed
*/
public Set<DetailsConnectorChange> getAndResetConnectorChanges() {
Set<DetailsConnectorChange> changes = new HashSet<DetailsConnectorChange>();

// populate diff with added/changed
for (Entry<Integer, Component> entry : rowIndexToDetails.entrySet()) {
Component component = entry.getValue();
assert component != null : "rowIndexToDetails contains a null component";

Integer newIndex = entry.getKey();
Integer oldIndex = prevRowIndexToDetails.inverse().get(
component);

/*
* only attach components. Detaching already happened in
* destroyDetails.
*/
if (newIndex != null && oldIndex == null) {
assert unattachedComponents.contains(component) : "unattachedComponents does not contain component for index "
+ newIndex + " (" + component + ")";
component.setParent(grid);
unattachedComponents.remove(component);
}

if (!SharedUtil.equals(oldIndex, newIndex)) {
changes.add(new DetailsConnectorChange(component, oldIndex,
newIndex, emptyDetails.containsKey(component)));
}
}

// populate diff with removed
for (Entry<Integer, Component> entry : prevRowIndexToDetails
.entrySet()) {
Integer oldIndex = entry.getKey();
Component component = entry.getValue();
Integer newIndex = rowIndexToDetails.inverse().get(component);
if (newIndex == null) {
changes.add(new DetailsConnectorChange(null, oldIndex,
null, emptyDetails.containsValue(oldIndex)));
}
}

// reset diff map
prevRowIndexToDetails = HashBiMap.create(rowIndexToDetails);

return changes;
}

public void refresh(Object itemId) {
Component component = visibleDetailsComponents.get(itemId);
Integer rowIndex = null;
if (component != null) {
rowIndex = rowIndexToDetails.inverse().get(component);
destroyDetails(itemId);
} else {
rowIndex = emptyDetails.remove(itemId);
}

assert rowIndex != null : "Given itemId does not map to an "
+ "existing detail row (" + itemId + ")";
createDetails(itemId, rowIndex.intValue());
}

void setGrid(Grid grid) {
if (this.grid != null) {
throw new IllegalStateException("Grid may injected only once.");
}
this.grid = grid;
}
}

private final Indexed container;

private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
@@ -672,6 +949,14 @@ public class RpcDataProviderExtension extends AbstractExtension {
/** Size possibly changed with a bare ItemSetChangeEvent */
private boolean bareItemSetTriggeredSizeChange = false;

/**
* This map represents all the details that are user-defined as visible.
* This does not reflect the status in the DOM.
*/
private Set<Object> visibleDetails = new HashSet<Object>();

private final DetailComponentManager detailComponentManager = new DetailComponentManager();

/**
* Creates a new data provider using the given container.
*
@@ -814,6 +1099,10 @@ public class RpcDataProviderExtension extends AbstractExtension {
rowObject.put(GridState.JSONKEY_DATA, rowData);
rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));

if (visibleDetails.contains(itemId)) {
rowObject.put(GridState.JSONKEY_DETAILS_VISIBLE, true);
}

rowReference.set(itemId);

CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator();
@@ -863,9 +1152,12 @@ public class RpcDataProviderExtension extends AbstractExtension {
*
* @param component
* the remote data grid component to extend
* @param columnKeys
* the key mapper for columns
*/
public void extend(Grid component, KeyMapper<Object> columnKeys) {
this.columnKeys = columnKeys;
detailComponentManager.setGrid(component);
super.extend(component);
}

@@ -949,6 +1241,10 @@ public class RpcDataProviderExtension extends AbstractExtension {
JsonArray rowArray = Json.createArray();
rowArray.set(0, row);
rpc.setRowData(index, rowArray);

if (isDetailsVisible(itemId)) {
detailComponentManager.createDetails(itemId, index);
}
}
}

@@ -1071,4 +1367,84 @@ public class RpcDataProviderExtension extends AbstractExtension {
return Logger.getLogger(RpcDataProviderExtension.class.getName());
}

/**
* Marks a row's details to be visible or hidden.
* <p>
* If that row is currently in the client side's cache, this information
* will be sent over to the client.
*
* @since 7.5.0
* @param itemId
* the id of the item of which to change the details visibility
* @param visible
* <code>true</code> to show the details, <code>false</code> to
* hide
*/
public void setDetailsVisible(Object itemId, boolean visible) {
final boolean modified;

if (visible) {
modified = visibleDetails.add(itemId);

/*
* We don't want to create the component here, since the component
* might be out of view, and thus we don't know where the details
* should end up on the client side. This is also a great thing to
* optimize away, so that in case a lot of things would be opened at
* once, a huge chunk of data doesn't get sent over immediately.
*/

} else {
modified = visibleDetails.remove(itemId);

/*
* Here we can try to destroy the component no matter what. The
* component has been removed and should be detached from the
* component hierarchy. The details row will be closed on the client
* side automatically.
*/
detailComponentManager.destroyDetails(itemId);
}

int rowIndex = indexOf(itemId);
boolean modifiedRowIsActive = activeRowHandler.activeRange
.contains(rowIndex);
if (modified && modifiedRowIsActive) {
updateRowData(itemId);
}
}

/**
* Checks whether the details for a row is marked as visible.
*
* @since 7.5.0
* @param itemId
* the id of the item of which to check the visibility
* @return <code>true</code> iff the detials are visible for the item. This
* might return <code>true</code> even if the row is not currently
* visible in the DOM
*/
public boolean isDetailsVisible(Object itemId) {
return visibleDetails.contains(itemId);
}

public void refreshDetails() {
for (Object itemId : ImmutableSet.copyOf(visibleDetails)) {
detailComponentManager.refresh(itemId);
}
}

private int indexOf(Object itemId) {
/*
* It would be great if we could optimize this method away, since the
* normal usage of Grid doesn't need any indices to be known. It was
* already optimized away once, maybe we can do away with these as well.
*/
return container.indexOfId(itemId);
}

/** Gets the detail component manager for this data provider */
public DetailComponentManager getDetailComponentManager() {
return detailComponentManager;
}
}

+ 567
- 9
server/src/com/vaadin/ui/Grid.java View File

@@ -18,6 +18,7 @@ package com.vaadin.ui;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -52,10 +53,10 @@ 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.RpcDataProviderExtension.DetailComponentManager;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.fieldgroup.FieldGroup.BindException;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
import com.vaadin.data.sort.Sort;
@@ -74,6 +75,7 @@ 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.EncodeResult;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.KeyMapper;
@@ -173,6 +175,120 @@ import elemental.json.JsonValue;
public class Grid extends AbstractComponent implements SelectionNotifier,
SortNotifier, SelectiveRenderer, ItemClickNotifier {

/**
* An event listener for column visibility change events in the Grid.
*
* @since 7.5.0
*/
public interface ColumnVisibilityChangeListener extends Serializable {
/**
* Called when a column has become hidden or unhidden.
*
* @param event
*/
void columnVisibilityChanged(ColumnVisibilityChangeEvent event);
}

/**
* An event that is fired when a column's visibility changes.
*
* @since 7.5.0
*/
public static class ColumnVisibilityChangeEvent extends Component.Event {

private final Column column;
private final boolean userOriginated;
private final boolean hidden;

/**
* Constructor for a column visibility change event.
*
* @param source
* the grid from which this event originates
* @param column
* the column that changed its visibility
* @param hidden
* <code>true</code> if the column was hidden,
* <code>false</code> if it became visible
* @param isUserOriginated
* <code>true</code> iff the event was triggered by an UI
* interaction
*/
public ColumnVisibilityChangeEvent(Grid source, Column column,
boolean hidden, boolean isUserOriginated) {
super(source);
this.column = column;
this.hidden = hidden;
userOriginated = isUserOriginated;
}

/**
* Gets the column that became hidden or visible.
*
* @return the column that became hidden or visible.
* @see Column#isHidden()
*/
public Column getColumn() {
return column;
}

/**
* Was the column set hidden or visible.
*
* @return <code>true</code> if the column was hidden <code>false</code>
* if it was set visible
*/
public boolean isHidden() {
return hidden;
}

/**
* Returns <code>true</code> if the column reorder was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}

/**
* A callback interface for generating details for a particular row in Grid.
*
* @since 7.5.0
* @author Vaadin Ltd
* @see DetailsGenerator#NULL
*/
public interface DetailsGenerator extends Serializable {

/** A details generator that provides no details */
public DetailsGenerator NULL = new DetailsGenerator() {
@Override
public Component getDetails(RowReference rowReference) {
return null;
}
};

/**
* This method is called for whenever a new details row needs to be
* generated.
* <p>
* <em>Note:</em> If a component gets generated, it may not be manually
* attached anywhere, nor may it be a reused instance &ndash; each
* invocation of this method should produce a unique and isolated
* component instance. Essentially, this should mostly be a
* self-contained fire-and-forget method, as external references to the
* generated component might cause unexpected behavior.
*
* @param rowReference
* the reference for the row for which to generate details
* @return the details for the given row, or <code>null</code> to leave
* the details empty.
*/
Component getDetails(RowReference rowReference);
}

/**
* Custom field group that allows finding property types before an item has
* been bound.
@@ -342,6 +458,58 @@ public class Grid extends AbstractComponent implements SelectionNotifier,

}

/**
* An event listener for column reorder events in the Grid.
*
* @since 7.5.0
*/
public interface ColumnReorderListener extends Serializable {
/**
* Called when the columns of the grid have been reordered.
*
* @param event
* An event providing more information
*/
void columnReorder(ColumnReorderEvent event);
}

/**
* An event that is fired when the columns are reordered.
*
* @since 7.5.0
*/
public static class ColumnReorderEvent extends Component.Event {

/**
* Is the column reorder related to this event initiated by the user
*/
private final boolean userOriginated;

/**
*
* @param source
* the grid where the event originated from
* @param userOriginated
* <code>true</code> if event is a result of user
* interaction, <code>false</code> if from API call
*/
public ColumnReorderEvent(Grid source, boolean userOriginated) {
super(source);
this.userOriginated = userOriginated;
}

/**
* Returns <code>true</code> if the column reorder was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}

}

/**
* Default error handler for the editor
*
@@ -2355,6 +2523,46 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
return this;
}

/**
* Gets the caption of the hiding toggle for this column.
*
* @since
* @see #setHidingToggleCaption(String)
* @return the caption for the hiding toggle for this column
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public String getHidingToggleCaption() throws IllegalStateException {
checkColumnIsAttached();
return state.hidingToggleCaption;
}

/**
* Sets the caption of the hiding toggle for this column. Shown in the
* toggle for this column in the grid's sidebar when the column is
* {@link #isHidable() hidable}.
* <p>
* By default, before triggering this setter, a user friendly version of
* the column's {@link #getPropertyId() property id} is used.
* <p>
* <em>NOTE:</em> setting this to <code>null</code> or empty string
* might cause the hiding toggle to not render correctly.
*
* @since
* @param hidingToggleCaption
* the text to show in the column hiding toggle
* @return the column itself
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column setHidingToggleCaption(String hidingToggleCaption)
throws IllegalStateException {
checkColumnIsAttached();
state.hidingToggleCaption = hidingToggleCaption;
grid.markAsDirty();
return this;
}

/**
* Returns the width (in pixels). By default a column is 100px wide.
*
@@ -2885,7 +3093,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
* 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.
* {@link com.vaadin.data.fieldgroup.FieldGroup.BindException
* BindException} to be thrown.
*
* @return the bound field; or <code>null</code> if the respective
* column is not editable
@@ -2901,13 +3110,79 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}

/**
* Hides or shows the column. By default columns are visible before
* explicitly hiding them.
*
* @since 7.5.0
* @param hidden
* <code>true</code> to hide the column, <code>false</code>
* to show
* @return this column
*/
public Column setHidden(boolean hidden) {
if (hidden != getState().hidden) {
getState().hidden = hidden;
grid.markAsDirty();
grid.fireColumnVisibilityChangeEvent(this, hidden, false);
}
return this;
}

/**
* Is this column hidden. Default is {@code false}.
*
* @since 7.5.0
* @return <code>true</code> if the column is currently hidden,
* <code>false</code> otherwise
*/
public boolean isHidden() {
return getState().hidden;
}

/**
* Set whether it is possible for the user to hide this column or not.
* Default is {@code false}.
* <p>
* <em>Note:</em> it is still possible to hide the column
* programmatically using {@link #setHidden(boolean)}
*
* @since 7.5.0
* @param hidable
* <code>true</code> iff the column may be hidable by the
* user via UI interaction
* @return this column
*/
public Column setHidable(boolean hidable) {
if (hidable != getState().hidable) {
getState().hidable = hidable;
grid.markAsDirty();
}
return this;
}

/**
* Is it possible for the the user to hide this column. Default is
* {@code false}.
* <p>
* <em>Note:</em> the column can be programmatically hidden using
* {@link #setHidden(boolean)} regardless of the returned value.
*
* @since 7.5.0
* @return <code>true</code> if the user can hide the column,
* <code>false</code> if not
*/
public boolean isHidable() {
return getState().hidable;
}

/*
* Writes the design attributes for this column into given element.
*
* @since
* @param design
* Element to write attributes into
* @param designContext
* the design context
*
* @param design Element to write attributes into
*
* @param designContext the design context
*/
protected void writeDesign(Element design, DesignContext designContext) {
Attributes attributes = design.attributes();
@@ -2925,6 +3200,14 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
getMaximumWidth(), def.maxWidth, Double.class);
DesignAttributeHandler.writeAttribute("expand", attributes,
getExpandRatio(), def.expandRatio, Integer.class);
DesignAttributeHandler.writeAttribute("hidable", attributes,
isHidable(), def.hidable, boolean.class);
DesignAttributeHandler.writeAttribute("hidden", attributes,
isHidden(), def.hidden, boolean.class);
DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
attributes, getHidingToggleCaption(),
SharedUtil.propertyIdToHumanFriendly(getPropertyId()),
String.class);
DesignAttributeHandler.writeAttribute("property-id", attributes,
getPropertyId(), null, Object.class);
}
@@ -2950,7 +3233,18 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
setEditable(DesignAttributeHandler.readAttribute("editable",
attributes, boolean.class));
}

if (design.hasAttr("hidable")) {
setHidable(DesignAttributeHandler.readAttribute("hidable",
attributes, boolean.class));
}
if (design.hasAttr("hidden")) {
setHidden(DesignAttributeHandler.readAttribute("hidden",
attributes, boolean.class));
}
if (design.hasAttr("hiding-toggle-caption")) {
setHidingToggleCaption(DesignAttributeHandler.readAttribute(
"hiding-toggle-caption", attributes, String.class));
}
// Read size info where necessary.
if (design.hasAttr("width")) {
setWidth(DesignAttributeHandler.readAttribute("width",
@@ -3202,12 +3496,30 @@ public class Grid extends AbstractComponent implements SelectionNotifier,

private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();

/**
* The user-defined details generator.
*
* @see #setDetailsGenerator(DetailsGenerator)
*/
private DetailsGenerator detailsGenerator = DetailsGenerator.NULL;

private DetailComponentManager detailComponentManager = null;

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);

private static final Method COLUMN_REORDER_METHOD = ReflectTools
.findMethod(ColumnReorderListener.class, "columnReorder",
ColumnReorderEvent.class);

private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
.findMethod(ColumnVisibilityChangeListener.class,
"columnVisibilityChanged",
ColumnVisibilityChangeEvent.class);

/**
* Creates a new Grid with a new {@link IndexedContainer} as the data
* source.
@@ -3402,6 +3714,87 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
fireEvent(new ItemClickEvent(Grid.this, item, itemId,
propertyId, details));
}

@Override
public void columnsReordered(List<String> newColumnOrder,
List<String> oldColumnOrder) {
final String diffStateKey = "columnOrder";
ConnectorTracker connectorTracker = getUI()
.getConnectorTracker();
JsonObject diffState = connectorTracker.getDiffState(Grid.this);
// discard the change if the columns have been reordered from
// the server side, as the server side is always right
if (getState(false).columnOrder.equals(oldColumnOrder)) {
// Don't mark as dirty since client has the state already
getState(false).columnOrder = newColumnOrder;
// write changes to diffState so that possible reverting the
// column order is sent to client
assert diffState.hasKey(diffStateKey) : "Field name has changed";
Type type = null;
try {
type = (getState(false).getClass().getDeclaredField(
diffStateKey).getGenericType());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
EncodeResult encodeResult = JsonCodec.encode(
getState(false).columnOrder, diffState, type,
connectorTracker);

diffState.put(diffStateKey, encodeResult.getEncodedValue());
fireColumnReorderEvent(true);
} else {
// make sure the client is reverted to the order that the
// server thinks it is
diffState.remove(diffStateKey);
markAsDirty();
}
}

@Override
public void columnVisibilityChanged(String id, boolean hidden,
boolean userOriginated) {
final Column column = getColumnByColumnId(id);
final GridColumnState columnState = column.getState();

if (columnState.hidden != hidden) {
columnState.hidden = hidden;

final String diffStateKey = "columns";
ConnectorTracker connectorTracker = getUI()
.getConnectorTracker();
JsonObject diffState = connectorTracker
.getDiffState(Grid.this);

assert diffState.hasKey(diffStateKey) : "Field name has changed";
Type type = null;
try {
type = (getState(false).getClass().getDeclaredField(
diffStateKey).getGenericType());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
EncodeResult encodeResult = JsonCodec.encode(
getState(false).columns, diffState, type,
connectorTracker);

diffState.put(diffStateKey, encodeResult.getEncodedValue());

fireColumnVisibilityChangeEvent(column, hidden,
userOriginated);
}
}

@Override
public void sendDetailsComponents(int fetchId) {
getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges(
detailComponentManager.getAndResetConnectorChanges(),
fetchId);
}
});

registerRpc(new EditorServerRpc() {
@@ -3565,6 +3958,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
datasourceExtension = new RpcDataProviderExtension(container);
datasourceExtension.extend(this, columnKeys);

detailComponentManager = datasourceExtension
.getDetailComponentManager();

/*
* selectionModel == null when the invocation comes from the
* constructor.
@@ -3783,6 +4179,31 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
return columnKeys.get(columnId);
}

/**
* Returns whether column reordering is allowed. Default value is
* <code>false</code>.
*
* @since 7.5.0
* @return true if reordering is allowed
*/
public boolean isColumnReorderingAllowed() {
return getState(false).columnReorderingAllowed;
}

/**
* Sets whether or not column reordering is allowed. Default value is
* <code>false</code>.
*
* @since 7.5.0
* @param columnReorderingAllowed
* specifies whether column reordering is allowed
*/
public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
if (isColumnReorderingAllowed() != columnReorderingAllowed) {
getState().columnReorderingAllowed = columnReorderingAllowed;
}
}

@Override
protected GridState getState() {
return (GridState) super.getState();
@@ -3818,8 +4239,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
header.addColumn(datasourcePropertyId);
footer.addColumn(datasourcePropertyId);

column.setHeaderCaption(SharedUtil.propertyIdToHumanFriendly(String
.valueOf(datasourcePropertyId)));
String humanFriendlyPropertyId = SharedUtil
.propertyIdToHumanFriendly(String.valueOf(datasourcePropertyId));
column.setHeaderCaption(humanFriendlyPropertyId);
column.setHidingToggleCaption(humanFriendlyPropertyId);

if (datasource instanceof Sortable
&& ((Sortable) datasource).getSortableContainerPropertyIds()
@@ -3910,6 +4333,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
columnOrder.addAll(stateColumnOrder);
}
getState().columnOrder = columnOrder;
fireColumnReorderEvent(false);
}

/**
@@ -3941,6 +4365,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
* 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.
* <p>
* <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
* columns} in the count.
*
* @see #setFrozenColumnCount(int)
*
@@ -3952,6 +4379,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,

/**
* Scrolls to a certain item, using {@link ScrollDestination#ANY}.
* <p>
* If the item has visible details, its size will also be taken into
* account.
*
* @param itemId
* id of item to scroll to.
@@ -3964,6 +4394,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,

/**
* Scrolls to a certain item, using user-specified scroll destination.
* <p>
* If the item has visible details, its size will also be taken into
* account.
*
* @param itemId
* id of item to scroll to.
@@ -4376,6 +4809,33 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
}

private void fireColumnReorderEvent(boolean userOriginated) {
fireEvent(new ColumnReorderEvent(this, userOriginated));
}

/**
* Registers a new column reorder listener.
*
* @since 7.5.0
* @param listener
* the listener to register
*/
public void addColumnReorderListener(ColumnReorderListener listener) {
addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD);
}

/**
* Removes a previously registered column reorder listener.
*
* @since 7.5.0
* @param listener
* the listener to remove
*/
public void removeColumnReorderListener(ColumnReorderListener listener) {
removeListener(ColumnReorderEvent.class, listener,
COLUMN_REORDER_METHOD);
}

/**
* Gets the
* {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper
@@ -4920,6 +5380,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}

componentList.addAll(getEditorFields());

componentList.addAll(detailComponentManager.getComponents());

return componentList.iterator();
}

@@ -5437,6 +5900,101 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
}

/**
* Registers a new column visibility change listener
*
* @since 7.5.0
* @param listener
* the listener to register
*/
public void addColumnVisibilityChangeListener(
ColumnVisibilityChangeListener listener) {
addListener(ColumnVisibilityChangeEvent.class, listener,
COLUMN_VISIBILITY_METHOD);
}

/**
* Removes a previously registered column visibility change listener
*
* @since 7.5.0
* @param listener
* the listener to remove
*/
public void removeColumnVisibilityChangeListener(
ColumnVisibilityChangeListener listener) {
removeListener(ColumnVisibilityChangeEvent.class, listener,
COLUMN_VISIBILITY_METHOD);
}

private void fireColumnVisibilityChangeEvent(Column column, boolean hidden,
boolean isUserOriginated) {
fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
isUserOriginated));
}

/**
* Sets a new details generator for row details.
* <p>
* The currently opened row details will be re-rendered.
*
* @since 7.5.0
* @param detailsGenerator
* the details generator to set
* @throws IllegalArgumentException
* if detailsGenerator is <code>null</code>;
*/
public void setDetailsGenerator(DetailsGenerator detailsGenerator)
throws IllegalArgumentException {
if (detailsGenerator == null) {
throw new IllegalArgumentException(
"Details generator may not be null");
} else if (detailsGenerator == this.detailsGenerator) {
return;
}

this.detailsGenerator = detailsGenerator;

datasourceExtension.refreshDetails();
getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges(
detailComponentManager.getAndResetConnectorChanges(), -1);
}

/**
* Gets the current details generator for row details.
*
* @since 7.5.0
* @return the detailsGenerator the current details generator
*/
public DetailsGenerator getDetailsGenerator() {
return detailsGenerator;
}

/**
* Shows or hides the details for a specific item.
*
* @since 7.5.0
* @param itemId
* the id of the item for which to set details visibility
* @param visible
* <code>true</code> to show the details, or <code>false</code>
* to hide them
*/
public void setDetailsVisible(Object itemId, boolean visible) {
datasourceExtension.setDetailsVisible(itemId, visible);
}

/**
* Checks whether details are visible for the given item.
*
* @since 7.5.0
* @param itemId
* the id of the item for which to check details visibility
* @return <code>true</code> iff the details are visible
*/
public boolean isDetailsVisible(Object itemId) {
return datasourceExtension.isDetailsVisible(itemId);
}

protected SelectionMode getDefaultSelectionMode() {
return SelectionMode.SINGLE;
}

+ 8
- 0
server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java View File

@@ -28,6 +28,8 @@ public class GridColumnDeclarativeTest extends GridDeclarativeTestBase {
+ " <col sortable=true width='100' property-id='Column1'>"
+ " <col sortable=false max-width='200' expand='2' property-id='Column2'>"
+ " <col sortable=true editable=false min-width='15' expand='1' property-id='Column3'>"
+ " <col sortable=true hidable=true hiding-toggle-caption='col 4' property-id='Column4'>"
+ " <col sortable=true hidden=true property-id='Column5'>"
+ "</colgroup>" //
+ "<thead />" //
+ "</table></v-grid>";
@@ -37,6 +39,9 @@ public class GridColumnDeclarativeTest extends GridDeclarativeTestBase {
.setExpandRatio(2).setSortable(false);
grid.addColumn("Column3", String.class).setMinimumWidth(15)
.setExpandRatio(1).setEditable(false);
grid.addColumn("Column4", String.class).setHidable(true)
.setHidingToggleCaption("col 4");
grid.addColumn("Column5", String.class).setHidden(true);

// Remove the default header
grid.removeHeaderRow(grid.getDefaultHeaderRow());
@@ -53,6 +58,7 @@ public class GridColumnDeclarativeTest extends GridDeclarativeTestBase {
+ " <col sortable=true width='100' property-id='Column1'>"
+ " <col sortable=true max-width='200' expand='2'>" // property-id="property-1"
+ " <col sortable=true min-width='15' expand='1' property-id='Column3'>"
+ " <col sortable=true hidden=true hidable=true hiding-toggle-caption='col 4'>" // property-id="property-3"
+ "</colgroup>" //
+ "</table></v-grid>";
Grid grid = new Grid();
@@ -61,6 +67,8 @@ public class GridColumnDeclarativeTest extends GridDeclarativeTestBase {
.setExpandRatio(2);
grid.addColumn("Column3", String.class).setMinimumWidth(15)
.setExpandRatio(1);
grid.addColumn("property-3", String.class).setHidable(true)
.setHidden(true).setHidingToggleCaption("col 4");

testRead(design, grid);
}

+ 2
- 1
server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java View File

@@ -38,7 +38,7 @@ public class GridDeclarativeAttributeTest extends DeclarativeTestBase<Grid> {
public void testBasicAttributes() {

String design = "<v-grid editable='true' rows=20 frozen-columns=-1 "
+ "editor-save-caption='Tallenna' editor-cancel-caption='Peruuta'>";
+ "editor-save-caption='Tallenna' editor-cancel-caption='Peruuta' column-reordering-allowed=true>";

Grid grid = new Grid();
grid.setEditorEnabled(true);
@@ -47,6 +47,7 @@ public class GridDeclarativeAttributeTest extends DeclarativeTestBase<Grid> {
grid.setFrozenColumnCount(-1);
grid.setEditorSaveCaption("Tallenna");
grid.setEditorCancelCaption("Peruuta");
grid.setColumnReorderingAllowed(true);

testRead(design, grid);
testWrite(design, grid);

+ 6
- 1
server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java View File

@@ -147,7 +147,12 @@ public class GridDeclarativeTestBase extends DeclarativeTestBase<Grid> {
col2.isSortable());
assertEquals(baseError + "Editable", col1.isEditable(),
col2.isEditable());

assertEquals(baseError + "Hidable", col1.isHidable(),
col2.isHidable());
assertEquals(baseError + "Hidden", col1.isHidden(), col2.isHidden());
assertEquals(baseError + "HidingToggleCaption",
col1.getHidingToggleCaption(),
col2.getHidingToggleCaption());
}
}
}

+ 190
- 0
shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java View File

@@ -0,0 +1,190 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.Comparator;

import com.vaadin.shared.Connector;

/**
* A description of an indexing modification for a connector. This is used by
* Grid for internal bookkeeping updates.
*
* @since 7.5.0
* @author Vaadin Ltd
*/
public class DetailsConnectorChange implements Serializable {

public static final Comparator<DetailsConnectorChange> REMOVED_FIRST_COMPARATOR = new Comparator<DetailsConnectorChange>() {
@Override
public int compare(DetailsConnectorChange a, DetailsConnectorChange b) {
boolean deleteA = a.getNewIndex() == null;
boolean deleteB = b.getNewIndex() == null;
if (deleteA && !deleteB) {
return -1;
} else if (!deleteA && deleteB) {
return 1;
} else {
return 0;
}
}
};

private Connector connector;
private Integer oldIndex;
private Integer newIndex;
private boolean shouldStillBeVisible;

/** Create a new connector index change */
public DetailsConnectorChange() {
}

/**
* Convenience constructor for setting all the fields in one line.
* <p>
* Calling this constructor will also assert that the state of the pojo is
* consistent by internal assumptions.
*
* @param connector
* the changed connector
* @param oldIndex
* the old index
* @param newIndex
* the new index
* @param shouldStillBeVisible
* details should be visible regardless of {@code connector}
*/
public DetailsConnectorChange(Connector connector, Integer oldIndex,
Integer newIndex, boolean shouldStillBeVisible) {
this.connector = connector;
this.oldIndex = oldIndex;
this.newIndex = newIndex;
this.shouldStillBeVisible = shouldStillBeVisible;

assert assertStateIsOk();
}

private boolean assertStateIsOk() {
boolean connectorAndNewIndexIsNotNull = connector != null
&& newIndex != null;
boolean connectorAndNewIndexIsNullThenOldIndexIsSet = connector == null
&& newIndex == null && oldIndex != null;

assert (connectorAndNewIndexIsNotNull || connectorAndNewIndexIsNullThenOldIndexIsSet) : "connector: "
+ nullityString(connector)
+ ", oldIndex: "
+ nullityString(oldIndex)
+ ", newIndex: "
+ nullityString(newIndex);
return true;
}

private static String nullityString(Object object) {
return object == null ? "null" : "non-null";
}

/**
* Gets the old index for the connector.
* <p>
* If <code>null</code>, the connector is recently added. This means that
* {@link #getConnector()} is expected not to return <code>null</code>.
*
* @return the old index for the connector
*/
public Integer getOldIndex() {
assert assertStateIsOk();
return oldIndex;
}

/**
* Gets the new index for the connector.
* <p>
* If <code>null</code>, the connector should be removed. This means that
* {@link #getConnector()} is expected to return <code>null</code> as well.
*
* @return the new index for the connector
*/
public Integer getNewIndex() {
assert assertStateIsOk();
return newIndex;
}

/**
* Gets the changed connector.
*
* @return the changed connector. Might be <code>null</code>
*/
public Connector getConnector() {
assert assertStateIsOk();
return connector;
}

/**
* Sets the changed connector.
*
* @param connector
* the changed connector. May be <code>null</code>
*/
public void setConnector(Connector connector) {
this.connector = connector;
}

/**
* Sets the old index
*
* @param oldIndex
* the old index. May be <code>null</code> if a new connector is
* being inserted
*/
public void setOldIndex(Integer oldIndex) {
this.oldIndex = oldIndex;
}

/**
* Sets the new index
*
* @param newIndex
* the new index. May be <code>null</code> if a connector is
* being removed
*/
public void setNewIndex(Integer newIndex) {
this.newIndex = newIndex;
}

/**
* Checks whether whether the details should remain open, even if connector
* might be <code>null</code>.
*
* @return <code>true</code> iff the details should remain open, even if
* connector might be <code>null</code>
*/
public boolean isShouldStillBeVisible() {
return shouldStillBeVisible;
}

/**
* Sets whether the details should remain open, even if connector might be
* <code>null</code>.
*
* @param shouldStillBeVisible
* <code>true</code> iff the details should remain open, even if
* connector might be <code>null</code>
*/
public void setShouldStillBeVisible(boolean shouldStillBeVisible) {
this.shouldStillBeVisible = shouldStillBeVisible;
}
}

+ 18
- 1
shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java View File

@@ -15,6 +15,8 @@
*/
package com.vaadin.shared.ui.grid;

import java.util.Set;

import com.vaadin.shared.communication.ClientRpc;

/**
@@ -26,7 +28,8 @@ import com.vaadin.shared.communication.ClientRpc;
public interface GridClientRpc extends ClientRpc {

/**
* Command client Grid to scroll to a specific data row.
* Command client Grid to scroll to a specific data row and its (optional)
* details.
*
* @param row
* zero-based row index. If the row index is below zero or above
@@ -55,4 +58,18 @@ public interface GridClientRpc extends ClientRpc {
*/
public void recalculateColumnWidths();

/**
* Informs the GridConnector on how the indexing of details connectors has
* changed.
*
* @since 7.5.0
* @param connectorChanges
* the indexing changes of details connectors
* @param fetchId
* the id of the request for fetching the changes. A negative
* number indicates a push (not requested by the client side)
*/
public void setDetailsConnectorChanges(
Set<DetailsConnectorChange> connectorChanges, int fetchId);

}

+ 9
- 0
shared/src/com/vaadin/shared/ui/grid/GridColumnState.java View File

@@ -76,4 +76,13 @@ public class GridColumnState implements Serializable {
* minWidth is less than the calculated width, minWidth will win.
*/
public double minWidth = GridConstants.DEFAULT_MIN_WIDTH;

/** Is the column currently hidden. */
public boolean hidden = false;

/** Can the column be hidden by the UI. */
public boolean hidable = false;

/** The caption for the column hiding toggle. */
public String hidingToggleCaption;
}

+ 44
- 0
shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java View File

@@ -47,4 +47,48 @@ public interface GridServerRpc extends ServerRpc {
* mouse event details
*/
void itemClick(String rowKey, String columnId, MouseEventDetails details);

/**
* Informs the server that the columns of the Grid have been reordered.
*
* @since 7.5.0
* @param newColumnOrder
* a list of column ids in the new order
* @param oldColumnOrder
* a list of column ids in order before the change
*/
void columnsReordered(List<String> newColumnOrder,
List<String> oldColumnOrder);

/**
* This is a trigger for Grid to send whatever has changed regarding the
* details components.
* <p>
* The components can't be sent eagerly, since they are generated as a side
* effect in
* {@link com.vaadin.data.RpcDataProviderExtension#beforeClientResponse(boolean)}
* , and that is too late to change the hierarchy. So we need this
* round-trip to work around that limitation.
*
* @since 7.5.0
* @param fetchId
* an unique identifier for the request
* @see com.vaadin.ui.Grid#setDetailsVisible(Object, boolean)
*/
void sendDetailsComponents(int fetchId);

/**
* Informs the server that the column's visibility has been changed.
*
* @since 7.5.0
* @param id
* the id of the column
* @param hidden
* <code>true</code> if hidden, <code>false</code> if unhidden
* @param userOriginated
* <code>true</code> if triggered by user, <code>false</code> if
* by code
*/
void columnVisibilityChanged(String id, boolean hidden,
boolean userOriginated);
}

+ 14
- 0
shared/src/com/vaadin/shared/ui/grid/GridState.java View File

@@ -102,6 +102,16 @@ public class GridState extends AbstractComponentState {
*/
public static final String JSONKEY_CELLSTYLES = "cs";

/**
* The key that tells whether details are visible for the row
*
* @see com.vaadin.ui.Grid#setDetailsGenerator(com.vaadin.ui.Grid.DetailsGenerator)
* @see com.vaadin.ui.Grid#setDetailsVisible(Object, boolean)
* @see com.vaadin.shared.data.DataProviderRpc#setRowData(int,
* elemental.json.JsonArray)
* */
public static final String JSONKEY_DETAILS_VISIBLE = "dv";

/**
* Columns in grid.
*/
@@ -156,4 +166,8 @@ public class GridState extends AbstractComponentState {
/** The caption for the cancel button in the editor */
@DelegateToWidget
public String editorCancelCaption = GridConstants.DEFAULT_CANCEL_CAPTION;

/** Whether the columns can be reordered */
@DelegateToWidget
public boolean columnReorderingAllowed;
}

+ 0
- 367
uitest/src/com/vaadin/testbench/elements/GridElement.java View File

@@ -1,367 +0,0 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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;
import com.vaadin.testbench.elementsbase.AbstractElement;
import com.vaadin.testbench.elementsbase.ServerClass;

/**
* 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
* the column index
* @return the editor field for given location
*
* @throws NoSuchElementException
* if {@code isEditable(colIndex) == false}
*/
public TestBenchElement getField(int colIndex) {
return grid.getSubPart("#editor[" + colIndex + "]");
}

/**
* Gets whether the column with the given index is editable, that is,
* has an associated editor field.
*
* @param colIndex
* the column index
* @return {@code true} if the column has an editor field, {@code false}
*  otherwise
*/
public boolean isEditable(int colIndex) {
return grid
.isElementPresent(By.vaadin("#editor[" + colIndex + "]"));
}

/**
* Checks whether a field is marked with an error.
*
* @param colIndex
* column index
* @return <code>true</code> iff the field is marked with an error
*/
public boolean isFieldErrorMarked(int colIndex) {
return getField(colIndex).getAttribute("class").contains("error");
}

/**
* Saves the fields of this editor.
* <p>
* <em>Note:</em> that this closes the editor making this element
* useless.
*/
public void save() {
findElement(By.className("v-grid-editor-save")).click();
}

/**
* Cancels this editor.
* <p>
* <em>Note:</em> that this closes the editor making this element
* useless.
*/
public void cancel() {
findElement(By.className("v-grid-editor-cancel")).click();
}

/**
* Gets the error message text, or <code>null</code> if no message is
* present.
*/
public String getErrorMessage() {
WebElement messageWrapper = findElement(By
.className("v-grid-editor-message"));
List<WebElement> divs = messageWrapper.findElements(By
.tagName("div"));
if (divs.isEmpty()) {
return null;
} else {
return divs.get(0).getText();
}
}
}

/**
* 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 TestBenchElement 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 TestBenchElement getFooterRow(int rowIndex) {
return getSubPart("#footer[" + rowIndex + "]");
}

/**
* Get the vertical scroll element
*
* @return The element representing the vertical scrollbar
*/
public TestBenchElement getVerticalScroller() {
List<WebElement> rootElements = findElements(By.xpath("./div"));
return (TestBenchElement) rootElements.get(0);
}

/**
* Get the horizontal scroll element
*
* @return The element representing the horizontal scrollbar
*/
public TestBenchElement getHorizontalScroller() {
List<WebElement> rootElements = findElements(By.xpath("./div"));
return (TestBenchElement) rootElements.get(1);
}

/**
* Get the header element
*
* @return The thead element
*/
public TestBenchElement getHeader() {
return getSubPart("#header");
}

/**
* Get the body element
*
* @return the tbody element
*/
public TestBenchElement getBody() {
return getSubPart("#cell");
}

/**
* Get the footer element
*
* @return the tfoot element
*/
public TestBenchElement getFooter() {
return getSubPart("#footer");
}

/**
* Get the element wrapping the table element
*
* @return The element that wraps the table element
*/
public TestBenchElement getTableWrapper() {
List<WebElement> rootElements = findElements(By.xpath("./div"));
return (TestBenchElement) 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));
}
}

+ 121
- 0
uitest/src/com/vaadin/tests/components/grid/GridDetailsLocation.java View File

@@ -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;

import com.vaadin.annotations.Theme;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.server.VaadinRequest;
import com.vaadin.tests.util.Person;
import com.vaadin.tests.util.PersonContainer;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.DetailsGenerator;
import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.SelectionMode;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;

@Theme(ValoTheme.THEME_NAME)
public class GridDetailsLocation extends UI {

private final DetailsGenerator detailsGenerator = new DetailsGenerator() {
@Override
public Component getDetails(RowReference rowReference) {
Person person = (Person) rowReference.getItemId();
Label label = new Label(person.getFirstName() + " "
+ person.getLastName());
// currently the decorator row doesn't change its height when the
// content height is different.
label.setHeight("30px");
return label;
}
};

private TextField numberTextField;
private Grid grid;

@Override
protected void init(VaadinRequest request) {

Layout layout = new VerticalLayout();

grid = new Grid(PersonContainer.createWithTestData(1000));
grid.setSelectionMode(SelectionMode.NONE);
layout.addComponent(grid);

final CheckBox checkbox = new CheckBox("Details generator");
checkbox.addValueChangeListener(new ValueChangeListener() {
@Override
@SuppressWarnings("boxing")
public void valueChange(ValueChangeEvent event) {
if (checkbox.getValue()) {
grid.setDetailsGenerator(detailsGenerator);
} else {
grid.setDetailsGenerator(DetailsGenerator.NULL);
}
}
});
layout.addComponent(checkbox);

numberTextField = new TextField("Row");
numberTextField.setImmediate(true);
layout.addComponent(numberTextField);

layout.addComponent(new Button("Toggle and scroll",
new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
toggle();
scrollTo();
}
}));
layout.addComponent(new Button("Scroll and toggle",
new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
scrollTo();
toggle();
}
}));

setContent(layout);
}

private void toggle() {
Object itemId = getItemId();
boolean isVisible = grid.isDetailsVisible(itemId);
grid.setDetailsVisible(itemId, !isVisible);
}

private void scrollTo() {
grid.scrollTo(getItemId());
}

private Object getItemId() {
int row = Integer.parseInt(numberTextField.getValue());
Object itemId = grid.getContainerDataSource().getIdByIndex(row);
return itemId;
}

}

+ 361
- 0
uitest/src/com/vaadin/tests/components/grid/GridDetailsLocationTest.java View File

@@ -0,0 +1,361 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.ArrayList;
import java.util.Collection;
import java.util.List;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedCondition;

import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.testbench.elements.CheckBoxElement;
import com.vaadin.testbench.elements.GridElement.GridRowElement;
import com.vaadin.testbench.elements.TextFieldElement;
import com.vaadin.testbench.parallel.Browser;
import com.vaadin.testbench.parallel.TestCategory;
import com.vaadin.tests.components.grid.basicfeatures.element.CustomGridElement;
import com.vaadin.tests.tb3.MultiBrowserTest;

@TestCategory("grid")
public class GridDetailsLocationTest extends MultiBrowserTest {

private static final int detailsDefaultHeight = 51;
private static final int detailsDefinedHeight = 33;
private static final int detailsDefinedHeightIE8 = 31;

private static class Param {
private final int rowIndex;
private final boolean useGenerator;
private final boolean scrollFirstToBottom;

public Param(int rowIndex, boolean useGenerator,
boolean scrollFirstToBottom) {
this.rowIndex = rowIndex;
this.useGenerator = useGenerator;
this.scrollFirstToBottom = scrollFirstToBottom;
}

public int getRowIndex() {
return rowIndex;
}

public boolean useGenerator() {
return useGenerator;
}

public boolean scrollFirstToBottom() {
return scrollFirstToBottom;
}

@Override
public String toString() {
return "Param [rowIndex=" + getRowIndex() + ", useGenerator="
+ useGenerator() + ", scrollFirstToBottom="
+ scrollFirstToBottom() + "]";
}

}

public static Collection<Param> parameters() {
List<Param> data = new ArrayList<Param>();

int[] params = new int[] { 0, 500, 999 };

for (int rowIndex : params) {

data.add(new Param(rowIndex, false, false));
data.add(new Param(rowIndex, true, false));
data.add(new Param(rowIndex, false, true));
data.add(new Param(rowIndex, true, true));
}

return data;
}

@Before
public void setUp() {
setDebug(true);
}

@Test
public void toggleAndScroll() throws Throwable {
for (Param param : parameters()) {
try {
openTestURL();
useGenerator(param.useGenerator());
scrollToBottom(param.scrollFirstToBottom());

// the tested method
toggleAndScroll(param.getRowIndex());

verifyLocation(param);
} catch (Throwable t) {
throw new Throwable("" + param, t);
}
}
}

@Test
public void scrollAndToggle() throws Throwable {
for (Param param : parameters()) {
try {
openTestURL();
useGenerator(param.useGenerator());
scrollToBottom(param.scrollFirstToBottom());

// the tested method
scrollAndToggle(param.getRowIndex());

verifyLocation(param);

} catch (Throwable t) {
throw new Throwable("" + param, t);
}
}
}

@Test
public void testDetailsHeightWithNoGenerator() {
openTestURL();
toggleAndScroll(5);

verifyDetailsRowHeight(5, detailsDefaultHeight, 0);
verifyDetailsDecoratorLocation(5, 0, 0);

toggleAndScroll(0);

verifyDetailsRowHeight(0, detailsDefaultHeight, 0);
verifyDetailsDecoratorLocation(0, 0, 1);

verifyDetailsRowHeight(5, detailsDefaultHeight, 1);
verifyDetailsDecoratorLocation(5, 1, 0);
}

@Test
public void testDetailsHeightWithGenerator() {
openTestURL();
useGenerator(true);
toggleAndScroll(5);

verifyDetailsRowHeight(5, getDefinedHeight(), 0);
verifyDetailsDecoratorLocation(5, 0, 0);

toggleAndScroll(0);

verifyDetailsRowHeight(0, getDefinedHeight(), 0);
// decorator elements are in DOM in the order they have been added
verifyDetailsDecoratorLocation(0, 0, 1);

verifyDetailsRowHeight(5, getDefinedHeight(), 1);
verifyDetailsDecoratorLocation(5, 1, 0);
}

private int getDefinedHeight() {
boolean ie8 = isIE8();
return ie8 ? detailsDefinedHeightIE8 : detailsDefinedHeight;
}

private void verifyDetailsRowHeight(int rowIndex, int expectedHeight,
int visibleIndexOfSpacer) {
waitForDetailsVisible();
WebElement details = getDetailsElement(visibleIndexOfSpacer);
Assert.assertEquals("Wrong details row height", expectedHeight, details
.getSize().getHeight());
}

private void verifyDetailsDecoratorLocation(int row,
int visibleIndexOfSpacer, int visibleIndexOfDeco) {
WebElement detailsElement = getDetailsElement(visibleIndexOfSpacer);
WebElement detailsDecoElement = getDetailsDecoElement(visibleIndexOfDeco);
GridRowElement rowElement = getGrid().getRow(row);

Assert.assertEquals(
"Details deco top position does not match row top pos",
rowElement.getLocation().getY(), detailsDecoElement
.getLocation().getY());
Assert.assertEquals(
"Details deco bottom position does not match details bottom pos",
detailsElement.getLocation().getY()
+ detailsElement.getSize().getHeight(),
detailsDecoElement.getLocation().getY()
+ detailsDecoElement.getSize().getHeight());
}

private void verifyLocation(Param param) {
Assert.assertFalse("Notification was present",
isElementPresent(By.className("v-Notification")));

TestBenchElement headerRow = getGrid().getHeaderRow(0);
final int topBoundary = headerRow.getLocation().getX()
+ headerRow.getSize().height;
final int bottomBoundary = getGrid().getLocation().getX()
+ getGrid().getSize().getHeight()
- getHorizontalScrollbar().getSize().height;

GridRowElement row = getGrid().getRow(param.getRowIndex());
final int rowTop = row.getLocation().getX();

waitForDetailsVisible();
WebElement details = getDetailsElement();
final int detailsBottom = details.getLocation().getX()
+ details.getSize().getHeight();

assertGreaterOrEqual("Row top should be inside grid, gridTop:"
+ topBoundary + " rowTop" + rowTop, topBoundary, rowTop);
assertLessThanOrEqual(
"Decorator bottom should be inside grid, gridBottom:"
+ bottomBoundary + " decoratorBotton:" + detailsBottom,
detailsBottom, bottomBoundary);

verifyDetailsRowHeight(param.getRowIndex(),
param.useGenerator() ? getDefinedHeight()
: detailsDefaultHeight, 0);
verifyDetailsDecoratorLocation(param.getRowIndex(), 0, 0);

Assert.assertFalse("Notification was present",
isElementPresent(By.className("v-Notification")));
}

private final By locator = By.className("v-grid-spacer");

private WebElement getDetailsElement() {
return getDetailsElement(0);
}

private WebElement getDetailsElement(int index) {
return findElements(locator).get(index);
}

private WebElement getDetailsDecoElement(int index) {
return findElements(By.className("v-grid-spacer-deco")).get(index);
}

private void waitForDetailsVisible() {
waitUntil(new ExpectedCondition<WebElement>() {

@Override
public WebElement apply(WebDriver driver) {
try {
WebElement detailsElement = getDetailsElement();
return detailsElement.isDisplayed()
&& detailsElement.getSize().getHeight() > 3 ? detailsElement
: null;
} catch (StaleElementReferenceException e) {
return null;
}
}

@Override
public String toString() {
return "visibility of element located by " + locator;
}

}, 5);
waitForElementVisible(By.className("v-grid-spacer"));
}

private void scrollToBottom(boolean scrollFirstToBottom) {
if (scrollFirstToBottom) {
executeScript("arguments[0].scrollTop = 9999999",
getVerticalScrollbar());
}
}

private void useGenerator(boolean use) {
CheckBoxElement checkBox = $(CheckBoxElement.class).first();
boolean isChecked = isCheckedValo(checkBox);
if (use != isChecked) {
clickValo(checkBox);
}
}

private boolean isIE8() {
DesiredCapabilities desiredCapabilities = getDesiredCapabilities();
DesiredCapabilities ie8Capabilities = Browser.IE8
.getDesiredCapabilities();
return desiredCapabilities.getBrowserName().equals(
ie8Capabilities.getBrowserName())
&& desiredCapabilities.getVersion().equals(
ie8Capabilities.getVersion());
}

@SuppressWarnings("boxing")
private boolean isCheckedValo(CheckBoxElement checkBoxElement) {
WebElement checkbox = checkBoxElement.findElement(By.tagName("input"));
Object value = executeScript("return arguments[0].checked;", checkbox);
return (Boolean) value;
}

private void clickValo(CheckBoxElement checkBoxElement) {
checkBoxElement.findElement(By.tagName("label")).click();
}

private Object executeScript(String string, Object... param) {
return ((JavascriptExecutor) getDriver()).executeScript(string, param);
}

private void scrollAndToggle(int row) {
setRow(row);
getScrollAndToggle().click();
}

private void toggleAndScroll(int row) {
setRow(row);
getToggleAndScroll().click();
}

private ButtonElement getScrollAndToggle() {
return $(ButtonElement.class).caption("Scroll and toggle").first();
}

private ButtonElement getToggleAndScroll() {
return $(ButtonElement.class).caption("Toggle and scroll").first();
}

private void setRow(int row) {
$(TextFieldElement.class).first().clear();
$(TextFieldElement.class).first().sendKeys(String.valueOf(row),
Keys.ENTER, Keys.TAB);
}

private CustomGridElement getGrid() {
return $(CustomGridElement.class).first();
}

private WebElement getVerticalScrollbar() {
WebElement scrollBar = getGrid().findElement(
By.className("v-grid-scroller-vertical"));
return scrollBar;
}

private WebElement getHorizontalScrollbar() {
WebElement scrollBar = getGrid().findElement(
By.className("v-grid-scroller-horizontal"));
return scrollBar;
}
}

+ 67
- 8
uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java View File

@@ -19,6 +19,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
@@ -29,15 +31,21 @@ import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.parallel.TestCategory;
import com.vaadin.tests.tb3.MultiBrowserTest;

@TestCategory("escalator")
@TestCategory("grid")
public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest {

private static final String LOGICAL_ROW_ATTRIBUTE_NAME = "vLogicalRow";
private static final String SPACER_CSS_CLASS = "v-escalator-spacer";

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 ADD_ONE_ROW_TO_END = "Add one row to end";
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_ALL_ROWS = "Remove all rows";
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";
@@ -48,6 +56,8 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest
protected static final String BODY_ROWS = "Body Rows";
protected static final String FOOTER_ROWS = "Footer Rows";

protected static final String SCROLL_TO = "Scroll to...";

protected static final String REMOVE_ALL_INSERT_SCROLL = "Remove all, insert 30 and scroll 40px";

protected static final String GENERAL = "General";
@@ -65,6 +75,20 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest
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";
protected static final String SET_100PX = "Set 100px";
protected static final String SPACERS = "Spacers";
protected static final String FOCUSABLE_UPDATER = "Focusable Updater";
protected static final String SCROLL_HERE_ANY_0PADDING = "Scroll here (ANY, 0)";
protected static final String SCROLL_HERE_SPACERBELOW_ANY_0PADDING = "Scroll here row+spacer below (ANY, 0)";
protected static final String REMOVE = "Remove";

protected static final String ROW_MINUS1 = "Row -1";
protected static final String ROW_0 = "Row 0";
protected static final String ROW_1 = "Row 1";
protected static final String ROW_25 = "Row 25";
protected static final String ROW_50 = "Row 50";
protected static final String ROW_75 = "Row 75";
protected static final String ROW_99 = "Row 99";

@Override
protected Class<?> getUIClass() {
@@ -163,15 +187,16 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest
private TestBenchElement getRow(String sectionTag, int row) {
TestBenchElement escalator = getEscalator();
WebElement tableSection = escalator.findElement(By.tagName(sectionTag));
By xpath;

String xpathExpression = "tr[not(@class='" + SPACER_CSS_CLASS + "')]";
if (row >= 0) {
int fromFirst = row + 1;
xpath = By.xpath("tr[" + fromFirst + "]");
xpathExpression += "[" + fromFirst + "]";
} else {
int fromLast = Math.abs(row + 1);
xpath = By.xpath("tr[last() - " + fromLast + "]");
xpathExpression += "[last() - " + fromLast + "]";
}
By xpath = By.xpath(xpathExpression);
if (tableSection != null
&& ((TestBenchElement) tableSection).isElementPresent(xpath)) {
return (TestBenchElement) tableSection.findElement(xpath);
@@ -235,17 +260,26 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest
}

protected void scrollVerticallyTo(int px) {
executeScript("arguments[0].scrollTop = " + px, getVerticalScrollbar());
getVerticalScrollbar().scroll(px);
}

protected TestBenchElement getVerticalScrollbar() {
protected long getScrollTop() {
return ((Long) executeScript("return arguments[0].scrollTop;",
getVerticalScrollbar())).longValue();
}

private TestBenchElement getVerticalScrollbar() {
return (TestBenchElement) getEscalator().findElement(
By.className("v-escalator-scroller-vertical"));
}

protected void scrollHorizontallyTo(int px) {
executeScript("arguments[0].scrollLeft = " + px,
getHorizontalScrollbar());
getHorizontalScrollbar().scrollLeft(px);
}

protected long getScrollLeft() {
return ((Long) executeScript("return arguments[0].scrollLeft;",
getHorizontalScrollbar())).longValue();
}

protected TestBenchElement getHorizontalScrollbar() {
@@ -260,4 +294,29 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest
protected void populate() {
selectMenuPath(GENERAL, POPULATE_COLUMN_ROW);
}

private List<WebElement> getSpacers() {
return getEscalator().findElements(By.className(SPACER_CSS_CLASS));
}

protected boolean spacersAreFoundInDom() {
List<WebElement> spacers = getSpacers();
return spacers != null && !spacers.isEmpty();
}

@SuppressWarnings("boxing")
protected WebElement getSpacer(int logicalRowIndex) {
List<WebElement> spacers = getSpacers();
System.out.println("size: " + spacers.size());
for (WebElement spacer : spacers) {
System.out.println(spacer + ", " + logicalRowIndex);
Boolean isInDom = (Boolean) executeScript("return arguments[0]['"
+ LOGICAL_ROW_ATTRIBUTE_NAME + "'] === arguments[1]",
spacer, logicalRowIndex);
if (isInDom) {
return spacer;
}
}
return null;
}
}

+ 19
- 3
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java View File

@@ -15,13 +15,15 @@
*/
package com.vaadin.tests.components.grid.basicfeatures;

import java.util.List;

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;
import com.vaadin.tests.components.grid.basicfeatures.element.CustomGridElement;

/**
* GridBasicClientFeatures.
@@ -79,13 +81,27 @@ public abstract class GridBasicClientFeaturesTest extends GridBasicFeaturesTest
}

@Override
protected GridElement getGridElement() {
protected CustomGridElement getGridElement() {
if (composite) {
// Composite requires the basic client features widget for subparts
return ((TestBenchElement) findElement(By
.vaadin("//TestWidgetComponent"))).wrap(GridElement.class);
.vaadin("//TestWidgetComponent")))
.wrap(CustomGridElement.class);
} else {
return super.getGridElement();
}
}

@Override
protected void assertColumnHeaderOrder(int... indices) {
List<TestBenchElement> headers = getGridHeaderRowCells();
for (int i = 0; i < indices.length; i++) {
assertColumnHeader("HEADER (0," + indices[i] + ")", headers.get(i));
}
}

protected void toggleColumnReorder() {
selectMenuPath("Component", "State", "Column Reordering");
}

}

+ 282
- 5
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java View File

@@ -46,10 +46,17 @@ 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.Component;
import com.vaadin.ui.CssLayout;
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.ColumnReorderEvent;
import com.vaadin.ui.Grid.ColumnReorderListener;
import com.vaadin.ui.Grid.ColumnVisibilityChangeEvent;
import com.vaadin.ui.Grid.ColumnVisibilityChangeListener;
import com.vaadin.ui.Grid.DetailsGenerator;
import com.vaadin.ui.Grid.FooterCell;
import com.vaadin.ui.Grid.HeaderCell;
import com.vaadin.ui.Grid.HeaderRow;
@@ -58,6 +65,9 @@ import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.RowStyleGenerator;
import com.vaadin.ui.Grid.SelectionMode;
import com.vaadin.ui.Grid.SelectionModel;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Panel;
import com.vaadin.ui.renderers.DateRenderer;
import com.vaadin.ui.renderers.HtmlRenderer;
import com.vaadin.ui.renderers.NumberRenderer;
@@ -109,6 +119,74 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}
};

private ColumnReorderListener columnReorderListener = new ColumnReorderListener() {

@Override
public void columnReorder(ColumnReorderEvent event) {
log("Columns reordered, userOriginated: "
+ event.isUserOriginated());
}
};

private ColumnVisibilityChangeListener columnVisibilityListener = new ColumnVisibilityChangeListener() {
@Override
public void columnVisibilityChanged(ColumnVisibilityChangeEvent event) {
log("Visibility changed: "//
+ "propertyId: " + event.getColumn().getPropertyId() //
+ ", isHidden: " + event.getColumn().isHidden() //
+ ", userOriginated: " + event.isUserOriginated());
}
};

private Panel detailsPanel;

private final DetailsGenerator detailedDetailsGenerator = new DetailsGenerator() {
@Override
public Component getDetails(final RowReference rowReference) {
CssLayout cssLayout = new CssLayout();
cssLayout.setHeight("200px");
cssLayout.setWidth("100%");

Item item = rowReference.getItem();
for (Object propertyId : item.getItemPropertyIds()) {
Property<?> prop = item.getItemProperty(propertyId);
String string = prop.getValue().toString();
cssLayout.addComponent(new Label(string));
}

final int rowIndex = grid.getContainerDataSource().indexOfId(
rowReference.getItemId());
ClickListener clickListener = new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
Notification.show("You clicked on the "
+ "button in the details for " + "row " + rowIndex);
}
};
cssLayout.addComponent(new Button("Press me", clickListener));
return cssLayout;
}
};

private final DetailsGenerator watchingDetailsGenerator = new DetailsGenerator() {
private int id = 0;

@Override
public Component getDetails(RowReference rowReference) {
return new Label("You are watching item id "
+ rowReference.getItemId() + " (" + (id++) + ")");
}
};

private final DetailsGenerator hierarchicalDetailsGenerator = new DetailsGenerator() {
@Override
public Component getDetails(RowReference rowReference) {
detailsPanel = new Panel();
detailsPanel.setContent(new Label("One"));
return detailsPanel;
}
};

@Override
@SuppressWarnings("unchecked")
protected Grid constructComponent() {
@@ -213,7 +291,9 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {

// Set varying column widths
for (int col = 0; col < COLUMNS; col++) {
grid.getColumn(getColumnProperty(col)).setWidth(100 + col * 50);
Column column = grid.getColumn(getColumnProperty(col));
column.setWidth(100 + col * 50);
column.setHidable(isColumnHidableByDefault(col));
}

grid.addSortListener(new SortListener() {
@@ -248,10 +328,35 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {

addFilterActions();

addInternalActions();

createDetailsActions();

this.grid = grid;
return grid;
}

protected boolean isColumnHidableByDefault(int col) {
return false;
}

protected boolean isColumnHiddenByDefault(int col) {
return false;
}

private void addInternalActions() {
createClickAction("Update column order without updating client",
"Internals", new Command<Grid, Void>() {
@Override
public void execute(Grid grid, Void value, Object data) {
List<Column> columns = grid.getColumns();
grid.setColumnOrder(columns.get(1).getPropertyId(),
columns.get(0).getPropertyId());
grid.getUI().getConnectorTracker().markClean(grid);
}
}, null);
}

private void addFilterActions() {
createClickAction("Column 1 starts with \"(23\"", "Filter",
new Command<Grid, Void>() {
@@ -494,6 +599,29 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}

});
createBooleanAction("ColumnReorderListener", "State", false,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid grid, Boolean value, Object data) {
if (value) {
grid.addColumnReorderListener(columnReorderListener);
} else {
grid.removeColumnReorderListener(columnReorderListener);
}
}
});
createBooleanAction("ColumnVisibilityChangeListener", "State", false,
new Command<Grid, Boolean>() {
@Override
public void execute(Grid grid, Boolean value, Object data) {
if (value) {
grid.addColumnVisibilityChangeListener(columnVisibilityListener);
} else {
grid.removeColumnVisibilityChangeListener(columnVisibilityListener);
}
}
});

createBooleanAction("Single select allow deselect", "State",
singleSelectAllowDeselect, new Command<Grid, Boolean>() {
@@ -508,6 +636,14 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}
}
});
createBooleanAction("Column Reordering Allowed", "State", false,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid c, Boolean value, Object data) {
c.setColumnReorderingAllowed(value);
}
});
}

protected void createHeaderActions() {
@@ -630,9 +766,9 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}, null);
}

@SuppressWarnings("boxing")
protected void createColumnActions() {
createCategory("Columns", null);

for (int c = 0; c < COLUMNS; c++) {
final int index = c;
createCategory(getColumnProperty(c), "Columns");
@@ -640,16 +776,55 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
createClickAction("Add / Remove", getColumnProperty(c),
new Command<Grid, String>() {

boolean wasHidable;
boolean wasHidden;
String wasColumnHidingToggleCaption;

@Override
public void execute(Grid grid, String value, Object data) {
String columnProperty = getColumnProperty((Integer) data);
if (grid.getColumn(columnProperty) == null) {
grid.addColumn(columnProperty);
Column column = grid.getColumn(columnProperty);
if (column == null) {
column = grid.addColumn(columnProperty);
column.setHidable(wasHidable);
column.setHidden(wasHidden);
column.setHidingToggleCaption(wasColumnHidingToggleCaption);
} else {
wasHidable = column.isHidable();
wasHidden = column.isHidden();
wasColumnHidingToggleCaption = column
.getHidingToggleCaption();
grid.removeColumn(columnProperty);
}
}
}, null, c);
createClickAction("Move left", getColumnProperty(c),
new Command<Grid, String>() {

@Override
public void execute(Grid grid, String value, Object data) {
final String columnProperty = getColumnProperty((Integer) data);
List<Column> cols = grid.getColumns();
List<Object> reordered = new ArrayList<Object>();
boolean addAsLast = false;
for (int i = 0; i < cols.size(); i++) {
Column col = cols.get(i);
if (col.getPropertyId().equals(columnProperty)) {
if (i == 0) {
addAsLast = true;
} else {
reordered.add(i - 1, columnProperty);
}
} else {
reordered.add(col.getPropertyId());
}
}
if (addAsLast) {
reordered.add(columnProperty);
}
grid.setColumnOrder(reordered.toArray());
}
}, null, c);

createBooleanAction("Sortable", getColumnProperty(c), true,
new Command<Grid, Boolean>() {
@@ -663,6 +838,37 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}
}, c);

createBooleanAction("Hidable", getColumnProperty(c),
isColumnHidableByDefault(c), new Command<Grid, Boolean>() {
@Override
public void execute(Grid c, Boolean hidable,
Object propertyId) {
grid.getColumn(propertyId).setHidable(hidable);
}
}, getColumnProperty(c));

createBooleanAction("Hidden", getColumnProperty(c),
isColumnHiddenByDefault(c), new Command<Grid, Boolean>() {
@Override
public void execute(Grid c, Boolean hidden,
Object propertyId) {
grid.getColumn(propertyId).setHidden(hidden);
}
}, getColumnProperty(c));
createClickAction("Change hiding toggle caption",
getColumnProperty(c), new Command<Grid, String>() {
int count = 0;

@Override
public void execute(Grid grid, String value, Object data) {
final String columnProperty = getColumnProperty((Integer) data);
grid.getColumn(columnProperty)
.setHidingToggleCaption(
columnProperty + " caption "
+ count++);
}
}, null, c);

createCategory("Column " + c + " Width", getColumnProperty(c));

createClickAction("Auto", "Column " + c + " Width",
@@ -680,7 +886,6 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
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)
@@ -778,6 +983,17 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {

}, c);
}
createBooleanAction("All columns hidable", "Columns", false,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid c, Boolean value, Object data) {
for (Column col : grid.getColumns()) {
col.setHidable(value);
}

}
});
}

private static String getColumnProperty(int c) {
@@ -1051,6 +1267,67 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}, null);
}

private void createDetailsActions() {
Command<Grid, DetailsGenerator> swapDetailsGenerator = new Command<Grid, DetailsGenerator>() {
@Override
public void execute(Grid c, DetailsGenerator generator, Object data) {
grid.setDetailsGenerator(generator);
}
};

Command<Grid, Boolean> openOrCloseItemId = new Command<Grid, Boolean>() {
@Override
@SuppressWarnings("boxing")
public void execute(Grid g, Boolean visible, Object itemId) {
g.setDetailsVisible(itemId, visible);
}
};

createCategory("Generators", "Details");
createClickAction("NULL", "Generators", swapDetailsGenerator,
DetailsGenerator.NULL);
createClickAction("\"Watching\"", "Generators", swapDetailsGenerator,
watchingDetailsGenerator);
createClickAction("Detailed", "Generators", swapDetailsGenerator,
detailedDetailsGenerator);
createClickAction("Hierarchical", "Generators", swapDetailsGenerator,
hierarchicalDetailsGenerator);

createClickAction("- Change Component", "Generators",
new Command<Grid, Void>() {
@Override
public void execute(Grid c, Void value, Object data) {
Label label = (Label) detailsPanel.getContent();
if (label.getValue().equals("One")) {
detailsPanel.setContent(new Label("Two"));
} else {
detailsPanel.setContent(new Label("One"));
}
}
}, null);

createClickAction("Toggle firstItemId", "Details",
new Command<Grid, Void>() {
@Override
public void execute(Grid g, Void value, Object data) {
Object firstItemId = g.getContainerDataSource()
.firstItemId();
boolean toggle = g.isDetailsVisible(firstItemId);
g.setDetailsVisible(firstItemId, !toggle);
g.setDetailsVisible(firstItemId, toggle);
}
}, null);

createBooleanAction("Open firstItemId", "Details", false,
openOrCloseItemId, ds.firstItemId());

createBooleanAction("Open 1", "Details", false, openOrCloseItemId,
ds.getIdByIndex(1));

createBooleanAction("Open 995", "Details", false, openOrCloseItemId,
ds.getIdByIndex(995));
}

@Override
protected Integer getTicketNumber() {
return 12829;

+ 136
- 3
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java View File

@@ -15,6 +15,9 @@
*/
package com.vaadin.tests.components.grid.basicfeatures;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;

@@ -25,13 +28,18 @@ import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;

import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.GridElement;
import com.vaadin.testbench.elements.GridElement.GridCellElement;
import com.vaadin.testbench.parallel.TestCategory;
import com.vaadin.tests.components.grid.basicfeatures.element.CustomGridElement;
import com.vaadin.tests.tb3.MultiBrowserTest;

@TestCategory("grid")
public abstract class GridBasicFeaturesTest extends MultiBrowserTest {

public enum CellSide {
LEFT, RIGHT;
}

@Override
protected boolean requireWindowFocusForIE() {
return true;
@@ -59,9 +67,9 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest {
}
}

protected GridElement getGridElement() {
protected CustomGridElement getGridElement() {
return ((TestBenchElement) findElement(By.id("testComponent")))
.wrap(GridElement.class);
.wrap(CustomGridElement.class);
}

protected void scrollGridVerticallyTo(double px) {
@@ -69,6 +77,11 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest {
getGridVerticalScrollbar());
}

protected void scrollGridHorizontallyTo(double px) {
executeScript("arguments[0].scrollLeft = " + px,
getGridHorizontalScrollbar());
}

protected int getGridVerticalScrollPos() {
return ((Number) executeScript("return arguments[0].scrollTop",
getGridVerticalScrollbar())).intValue();
@@ -117,6 +130,12 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest {
By.xpath("//div[contains(@class, \"v-grid-scroller-vertical\")]"));
}

protected WebElement getGridHorizontalScrollbar() {
return getDriver()
.findElement(
By.xpath("//div[contains(@class, \"v-grid-scroller-horizontal\")]"));
}

/**
* Reloads the page without restartApplication. This occasionally breaks
* stuff.
@@ -127,4 +146,118 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest {
testUrl = testUrl.replace("?&", "?");
driver.get(testUrl);
}

protected void focusCell(int row, int column) {
getGridElement().getCell(row, column).click();
}

protected void setFrozenColumns(int numberOfFrozenColumns) {
selectMenuPath("Component", "State", "Frozen column count",
Integer.toString(numberOfFrozenColumns));
}

protected void assertColumnHeaderOrder(int... indices) {
List<TestBenchElement> headers = getGridHeaderRowCells();
for (int i = 0; i < indices.length; i++) {
assertColumnHeader("Column " + indices[i], headers.get(i));
}
}

protected void assertColumnHeader(String expectedHeaderCaption,
TestBenchElement testBenchElement) {
assertEquals(expectedHeaderCaption.toLowerCase(), testBenchElement
.getText().toLowerCase());
}

protected GridCellElement getDefaultColumnHeader(int index) {
List<GridCellElement> headerRowCells = getGridElement().getHeaderCells(
0);
return headerRowCells.get(index);
}

protected void dragAndDropDefaultColumnHeader(int draggedColumnHeaderIndex,
int onTopOfColumnHeaderIndex, CellSide cellSide) {
GridCellElement columnHeader = getDefaultColumnHeader(onTopOfColumnHeaderIndex);
new Actions(getDriver())
.clickAndHold(getDefaultColumnHeader(draggedColumnHeaderIndex))
.moveToElement(
columnHeader,
getHorizontalOffsetForDragAndDrop(columnHeader,
cellSide), 0).release().perform();
}

private int getHorizontalOffsetForDragAndDrop(GridCellElement columnHeader,
CellSide cellSide) {
if (cellSide == CellSide.LEFT) {
return 5;
} else {
int half = columnHeader.getSize().getWidth() / 2;
return half + (half / 2);
}
}

protected void dragAndDropColumnHeader(int headerRow,
int draggedColumnHeaderIndex, int onTopOfColumnHeaderIndex,
CellSide cellSide) {
GridCellElement headerCell = getGridElement().getHeaderCell(headerRow,
onTopOfColumnHeaderIndex);
new Actions(getDriver())
.clickAndHold(
getGridElement().getHeaderCell(headerRow,
draggedColumnHeaderIndex))
.moveToElement(
headerCell,
getHorizontalOffsetForDragAndDrop(headerCell, cellSide),
0).release().perform();
}

protected void dragAndDropColumnHeader(int headerRow,
int draggedColumnHeaderIndex, int onTopOfColumnHeaderIndex,
int horizontalOffset) {
GridCellElement headerCell = getGridElement().getHeaderCell(headerRow,
onTopOfColumnHeaderIndex);
new Actions(getDriver())
.clickAndHold(
getGridElement().getHeaderCell(headerRow,
draggedColumnHeaderIndex))
.moveToElement(headerCell, horizontalOffset, 0).release()
.perform();
}

protected void assertColumnIsSorted(int index) {
WebElement columnHeader = getDefaultColumnHeader(index);
assertTrue(columnHeader.getAttribute("class").contains("sort"));
}

protected void assertFocusedCell(int row, int column) {
assertTrue(getGridElement().getCell(row, column).getAttribute("class")
.contains("focused"));
}

protected WebElement getSidebar() {
List<WebElement> elements = findElements(By.className("v-grid-sidebar"));
return elements.isEmpty() ? null : elements.get(0);
}

protected WebElement getSidebarOpenButton() {
List<WebElement> elements = findElements(By
.className("v-grid-sidebar-button"));
return elements.isEmpty() ? null : elements.get(0);
}

/**
* Returns the toggle inside the sidebar for hiding the column at the given
* index, or null if not found.
*/
protected WebElement getColumnHidingToggle(int columnIndex) {
WebElement sidebar = getSidebar();
List<WebElement> elements = sidebar.findElements(By
.className("column-hiding-toggle"));
for (WebElement e : elements) {
if ((e.getText().toLowerCase()).startsWith("column " + columnIndex)) {
return e;
}
}
return null;
}
}

+ 994
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java View File

@@ -0,0 +1,994 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.List;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
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.testbench.parallel.TestCategory;

@TestCategory("grid")
public class GridColumnHidingTest extends GridBasicClientFeaturesTest {

private static final String CAPTION_0_1 = "Join column cells 0, 1";
private static final String CAPTION_1_2 = "Join columns 1, 2";
private static final String CAPTION_3_4_5 = "Join columns 3, 4, 5";
private static final String CAPTION_ALL = "Join all columns";

@Before
public void before() {
openTestURL();
}

@Test
public void testColumnHiding_hidingColumnsFromAPI_works() {
selectMenuPath("Component", "State", "Width", "1000px");
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(1);
toggleHideColumnAPI(2);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(4, 5, 6, 7, 8);
}

@Test
public void testColumnHiding_unhidingColumnsFromAPI_works() {
selectMenuPath("Component", "State", "Width", "1000px");
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(1);
toggleHideColumnAPI(2);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 4, 5, 6, 7, 8);

toggleHideColumnAPI(1);
toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 2, 4, 5, 6);
}

@Test
public void testColumnHiding_hidingUnhidingFromAPI_works() {
selectMenuPath("Component", "State", "Width", "1000px");
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 3, 4, 5, 6);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 3, 4, 5, 6);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6);
}

@Test
public void testColumnHiding_changeVisibilityAPI_triggersClientSideEvent() {
assertColumnHeaderOrder(0, 1, 2, 3, 4);
selectMenuPath("Component", "Internals", "Listeners",
"Add Column Visibility Change listener");

toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 3, 4);

WebElement webElement = findElement(By.id("columnvisibility"));
int counter = Integer.parseInt(webElement.getAttribute("counter"));
int columnIndex = Integer.parseInt(webElement
.getAttribute("columnindex"));
boolean userOriginated = Boolean.parseBoolean(webElement
.getAttribute("useroriginated"));
boolean hidden = Boolean.parseBoolean(webElement
.getAttribute("ishidden"));

assertNotNull("no event fired", webElement);
assertEquals(1, counter);
assertEquals(2, columnIndex);
assertEquals(false, userOriginated);
assertEquals(true, hidden);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 2, 3, 4);

webElement = findElement(By.id("columnvisibility"));
counter = Integer.parseInt(webElement.getAttribute("counter"));
columnIndex = Integer.parseInt(webElement.getAttribute("columnIndex"));
userOriginated = Boolean.parseBoolean(webElement
.getAttribute("userOriginated"));
hidden = Boolean.parseBoolean(webElement.getAttribute("ishidden"));

assertNotNull("no event fired", webElement);
assertEquals(2, counter);
assertEquals(2, columnIndex);
assertEquals(false, userOriginated);
assertEquals(false, hidden);
}

@Test
public void testColumnHiding_changeVisibilityToggle_triggersClientSideEvent() {
assertColumnHeaderOrder(0, 1, 2, 3, 4);
selectMenuPath("Component", "Internals", "Listeners",
"Add Column Visibility Change listener");

toggleHidableColumnAPI(2);
clickSidebarOpenButton();
getColumnHidingToggle(2).click();
assertColumnHeaderOrder(0, 1, 3, 4);

WebElement webElement = findElement(By.id("columnvisibility"));
int counter = Integer.parseInt(webElement.getAttribute("counter"));
int columnIndex = Integer.parseInt(webElement
.getAttribute("columnindex"));
boolean userOriginated = Boolean.parseBoolean(webElement
.getAttribute("useroriginated"));
boolean hidden = Boolean.parseBoolean(webElement
.getAttribute("ishidden"));

assertNotNull("no event fired", webElement);
assertEquals(1, counter);
assertEquals(2, columnIndex);
assertEquals(true, userOriginated);
assertEquals(true, hidden);

getColumnHidingToggle(2).click();
assertColumnHeaderOrder(0, 1, 2, 3, 4);

webElement = findElement(By.id("columnvisibility"));
counter = Integer.parseInt(webElement.getAttribute("counter"));
columnIndex = Integer.parseInt(webElement.getAttribute("columnIndex"));
userOriginated = Boolean.parseBoolean(webElement
.getAttribute("userOriginated"));
hidden = Boolean.parseBoolean(webElement.getAttribute("ishidden"));

assertNotNull("no event fired", webElement);
assertEquals(2, counter);
assertEquals(2, columnIndex);
assertEquals(true, userOriginated);
assertEquals(false, hidden);
}

@Test
public void testColumnHidability_onTriggerColumnHidability_showsSidebarButton() {
WebElement sidebar = getSidebar();
assertNull(sidebar);

toggleHidableColumnAPI(0);

sidebar = getSidebar();
assertNotNull(sidebar);
}

@Test
public void testColumnHidability_triggeringColumnHidabilityWithSeveralColumns_showsAndHidesSiderbarButton() {
verifySidebarNotVisible();

toggleHidableColumnAPI(3);
toggleHidableColumnAPI(4);

verifySidebarVisible();

toggleHidableColumnAPI(3);

verifySidebarVisible();

toggleHidableColumnAPI(4);

verifySidebarNotVisible();
}

@Test
public void testColumnHidability_clickingSidebarButton_opensClosesSidebar() {
toggleHidableColumnAPI(0);
verifySidebarClosed();

clickSidebarOpenButton();

verifySidebarOpened();

clickSidebarOpenButton();

verifySidebarClosed();
}

@Test
public void testColumnHidability_settingColumnHidable_showsToggleInSidebar() {
toggleHidableColumnAPI(0);
verifySidebarClosed();
clickSidebarOpenButton();
verifySidebarOpened();

verifyColumnHidingOption(0, false);
}

@Test
public void testColumnHiding_hidingColumnWithToggle_works() {
assertColumnHeaderOrder(0, 1, 2, 3, 4);
toggleHidableColumnAPI(0);
verifySidebarClosed();
clickSidebarOpenButton();
verifySidebarOpened();
verifyColumnHidingOption(0, false);

getColumnHidingToggle(0).click();
verifyColumnHidingOption(0, true);
assertColumnHeaderOrder(1, 2, 3, 4);

getColumnHidingToggle(0).click();
verifyColumnHidingOption(0, false);
assertColumnHeaderOrder(0, 1, 2, 3, 4);
}

@Test
public void testColumnHiding_updatingHiddenWhileSidebarClosed_updatesToggleValue() {
toggleHidableColumnAPI(0);
toggleHidableColumnAPI(3);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 1, 2, 4);
verifySidebarClosed();

clickSidebarOpenButton();
verifySidebarOpened();
verifyColumnHidingOption(0, false);
verifyColumnHidingOption(3, true);

clickSidebarOpenButton();
verifySidebarClosed();

toggleHideColumnAPI(0);
toggleHideColumnAPI(3);

clickSidebarOpenButton();
verifySidebarOpened();
verifyColumnHidingOption(0, true);
verifyColumnHidingOption(3, false);

}

@Test
public void testColumnHiding_hidingMultipleColumnsWithToggle_hidesColumns() {
assertColumnHeaderOrder(0, 1, 2, 3, 4);

toggleHideColumnAPI(1);
toggleHidableColumnAPI(0);
toggleHidableColumnAPI(1);
toggleHidableColumnAPI(2);
toggleHidableColumnAPI(3);
toggleHidableColumnAPI(4);
verifySidebarClosed();
assertColumnHeaderOrder(0, 2, 3, 4);

clickSidebarOpenButton();
verifySidebarOpened();
verifyColumnHidingOption(0, false);
verifyColumnHidingOption(1, true);
verifyColumnHidingOption(2, false);
verifyColumnHidingOption(3, false);
verifyColumnHidingOption(4, false);

// must be done in a funny order so that the header indexes won't break
// (because of data source uses counter)
getColumnHidingToggle(1).click();
getColumnHidingToggle(2).click();
getColumnHidingToggle(3).click();
getColumnHidingToggle(4).click();
getColumnHidingToggle(0).click();
verifyColumnHidingOption(0, true);
verifyColumnHidingOption(1, false);
verifyColumnHidingOption(2, true);
verifyColumnHidingOption(3, true);
verifyColumnHidingOption(4, true);

assertColumnHeaderOrder(1, 5, 6, 7);

getColumnHidingToggle(0).click();
getColumnHidingToggle(2).click();
getColumnHidingToggle(1).click();
verifyColumnHidingOption(0, false);
verifyColumnHidingOption(1, true);
verifyColumnHidingOption(2, false);
assertColumnHeaderOrder(0, 2, 5, 6);
}

@Test
public void testColumnHidability_changingHidabilityWhenSidebarClosed_addsRemovesToggles() {
toggleHideColumnAPI(0);
toggleHideColumnAPI(4);
assertColumnHeaderOrder(1, 2, 3, 5);
toggleHidableColumnAPI(0);
toggleHidableColumnAPI(3);
toggleHidableColumnAPI(4);
verifySidebarClosed();

clickSidebarOpenButton();
verifySidebarOpened();
verifyColumnHidingOption(0, true);
verifyColumnHidingOption(3, false);
verifyColumnHidingOption(4, true);

clickSidebarOpenButton();
verifySidebarClosed();

toggleHidableColumnAPI(0);
toggleHidableColumnAPI(3);

verifySidebarClosed();
clickSidebarOpenButton();
verifySidebarOpened();
verifyColumnHidingOption(4, true);

assertNull(getColumnHidingToggle(0));
assertNull(getColumnHidingToggle(3));
}

@Test
public void testColumnHidability_togglingHidability_placesTogglesInRightOrder() {
toggleHidableColumnAPI(3);
toggleHidableColumnAPI(2);
clickSidebarOpenButton();

verifyColumnHidingTogglesOrder(2, 3);

toggleHidableColumnAPI(1);
toggleHidableColumnAPI(2);
toggleHidableColumnAPI(6);
toggleHidableColumnAPI(0);

verifyColumnHidingTogglesOrder(0, 1, 3, 6);

clickSidebarOpenButton();

toggleHidableColumnAPI(2);
toggleHidableColumnAPI(4);
toggleHidableColumnAPI(7);

clickSidebarOpenButton();

verifyColumnHidingTogglesOrder(0, 1, 2, 3, 4, 6, 7);
}

@Test
public void testColumnHidability_reorderingColumns_updatesColumnToggleOrder() {
selectMenuPath("Component", "State", "Width", "1000px");
toggleHidableColumnAPI(0);
toggleHidableColumnAPI(1);
toggleHidableColumnAPI(3);
toggleHidableColumnAPI(4);
clickSidebarOpenButton();
verifyColumnHidingTogglesOrder(0, 1, 3, 4);
clickSidebarOpenButton();

toggleColumnReorder();
dragAndDropColumnHeader(0, 3, 0, CellSide.LEFT);

assertColumnHeaderOrder(3, 0, 1, 2, 4);
clickSidebarOpenButton();
verifyColumnHidingTogglesOrder(3, 0, 1, 4);

clickSidebarOpenButton();
dragAndDropColumnHeader(0, 1, 3, CellSide.RIGHT);
dragAndDropColumnHeader(0, 4, 0, CellSide.LEFT);
dragAndDropColumnHeader(0, 3, 0, CellSide.LEFT);

assertColumnHeaderOrder(2, 4, 3, 1, 0);
clickSidebarOpenButton();
verifyColumnHidingTogglesOrder(4, 3, 1, 0);
}

@Test
public void testColumnHidingAndReorder_reorderingOverHiddenColumn_orderIsKept() {
selectMenuPath("Component", "State", "Width", "1000px");
toggleColumnReorder();
toggleHideColumnAPI(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5);

dragAndDropColumnHeader(0, 1, 0, CellSide.LEFT);
assertColumnHeaderOrder(2, 1, 3, 4, 5);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(0, 2, 1, 3, 4, 5);

toggleHideColumnAPI(1);
assertColumnHeaderOrder(0, 2, 3, 4, 5);

// right side of hidden column
dragAndDropColumnHeader(0, 0, 2, CellSide.LEFT);
assertColumnHeaderOrder(2, 0, 3, 4, 5);

toggleHideColumnAPI(1);
assertColumnHeaderOrder(2, 1, 0, 3, 4, 5);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(2, 1, 3, 4, 5);

// left side of hidden column
dragAndDropColumnHeader(0, 0, 1, CellSide.RIGHT);
assertColumnHeaderOrder(1, 2, 3, 4, 5);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(1, 0, 2, 3, 4, 5);
}

@Test
public void testColumnHidingAndReorder_reorderingWithMultipleHiddenColumns_works() {
selectMenuPath("Component", "State", "Width", "1000px");
toggleColumnReorder();
toggleHideColumnAPI(2);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 1, 4, 5, 6);

dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);
assertColumnHeaderOrder(1, 0, 4, 5, 6);

toggleHideColumnAPI(3);
assertColumnHeaderOrder(1, 3, 0, 4, 5, 6);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(1, 2, 3, 0, 4, 5, 6);

toggleHideColumnAPI(0);
toggleHideColumnAPI(4);
assertColumnHeaderOrder(1, 2, 3, 5, 6);

dragAndDropDefaultColumnHeader(4, 3, CellSide.LEFT);
assertColumnHeaderOrder(1, 2, 3, 6, 5);

dragAndDropDefaultColumnHeader(4, 2, CellSide.RIGHT);
assertColumnHeaderOrder(1, 2, 3, 5, 6);

toggleHideColumnAPI(0);
assertColumnHeaderOrder(1, 2, 3, 0, 5, 6);

toggleHideColumnAPI(4);
assertColumnHeaderOrder(1, 2, 3, 0, 4, 5, 6);
}

@Test
public void testReorderingHiddenColumns_movingHiddenColumn_indexIsUpdated() {
selectMenuPath("Component", "State", "Width", "1000px");
toggleHideColumnAPI(2);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 1, 4, 5, 6);

moveColumnLeft(3);
assertColumnHeaderOrder(0, 1, 4, 5, 6);

toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 1, 3, 4, 5, 6);
toggleHideColumnAPI(2);
assertColumnHeaderOrder(0, 1, 3, 2, 4, 5, 6);

toggleHideColumnAPI(2);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 1, 4, 5, 6);

moveColumnLeft(2);
moveColumnLeft(2);
moveColumnLeft(2);
assertColumnHeaderOrder(0, 1, 4, 5, 6);

toggleHideColumnAPI(2);
assertColumnHeaderOrder(2, 0, 1, 4, 5, 6);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(2, 0, 1, 3, 4, 5, 6);
}

// keyboard actions not working in client side test case?
@Test
@Ignore
public void testNavigationWithHiddenColumns_navigatingOverHiddenColumn_goesToNextVisibleColumn() {
selectMenuPath("Component", "State", "Width", "1000px");
toggleHideColumnAPI(2);
toggleHideColumnAPI(3);
assertColumnHeaderOrder(0, 1, 4, 5, 6);

getGridElement().getCell(2, 4).click();
GridCellElement cell = getGridElement().getCell(2, 4);
assertTrue(cell.isFocused());

new Actions(getDriver()).sendKeys(Keys.ARROW_LEFT);
cell = getGridElement().getCell(2, 1);
assertTrue(cell.isFocused());

new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT);
cell = getGridElement().getCell(2, 4);
assertTrue(cell.isFocused());
}

@Test
public void testNavigationWithHiddenColumns_hiddenFirstAndLastColumn_keepsNavigation() {
selectMenuPath("Component", "State", "Width", "1000px");
toggleHideColumnAPI(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5, 6);

getGridElement().getCell(2, 1).click();
assertTrue(getGridElement().getCell(2, 1).isFocused());

new Actions(getDriver()).sendKeys(Keys.ARROW_LEFT);
GridCellElement cell = getGridElement().getCell(2, 1);
assertTrue(cell.isFocused());

scrollGridHorizontallyTo(10000);

//
getGridElement().getHeaderCell(0, 9).click();
cell = getGridElement().getHeaderCell(0, 9);
assertTrue(cell.isFocused());
toggleHideColumnAPI(10);
toggleHideColumnAPI(11);

new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT);
new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT);
toggleHideColumnAPI(10);
toggleHideColumnAPI(11);
cell = getGridElement().getHeaderCell(0, 9);
assertTrue(cell.isFocused());
}

@Test
public void testFrozenColumnHiding_lastFrozenColumnHidden_isFrozenWhenMadeVisible() {
toggleFrozenColumns(2);
toggleHidableColumnAPI(0);
toggleHidableColumnAPI(1);
getSidebarOpenButton().click();
verifyColumnIsFrozen(0);
verifyColumnIsFrozen(1);
verifyColumnIsNotFrozen(2);
assertColumnHeaderOrder(0, 1, 2, 3);

getColumnHidingToggle(1).click();
verifyColumnIsFrozen(0);
// the grid element indexing doesn't take hidden columns into account!
verifyColumnIsNotFrozen(1);
assertColumnHeaderOrder(0, 2, 3);

getColumnHidingToggle(0).click();
verifyColumnIsNotFrozen(0);
assertColumnHeaderOrder(2, 3, 4);

getColumnHidingToggle(0).click();
assertColumnHeaderOrder(0, 2, 3);
verifyColumnIsFrozen(0);
verifyColumnIsNotFrozen(1);

getColumnHidingToggle(1).click();
assertColumnHeaderOrder(0, 1, 2, 3);
verifyColumnIsFrozen(0);
verifyColumnIsFrozen(1);
verifyColumnIsNotFrozen(2);
}

@Test
public void testFrozenColumnHiding_columnHiddenFrozenCountChanged_columnIsFrozenWhenVisible() {
toggleHidableColumnAPI(1);
toggleHidableColumnAPI(2);
getSidebarOpenButton().click();
getColumnHidingToggle(1).click();
getColumnHidingToggle(2).click();
assertColumnHeaderOrder(0, 3, 4);

toggleFrozenColumns(3);
verifyColumnIsFrozen(0);
// the grid element indexing doesn't take hidden columns into account!
verifyColumnIsNotFrozen(1);
verifyColumnIsNotFrozen(2);

getColumnHidingToggle(2).click();
verifyColumnIsFrozen(0);
verifyColumnIsFrozen(1);
verifyColumnIsNotFrozen(2);
verifyColumnIsNotFrozen(3);

getColumnHidingToggle(1).click();
verifyColumnIsFrozen(0);
verifyColumnIsFrozen(1);
verifyColumnIsFrozen(2);
verifyColumnIsNotFrozen(3);
verifyColumnIsNotFrozen(4);
}

@Test
public void testSpannedCells_hidingColumnInBeginning_rendersSpannedCellCorrectly() {
loadSpannedCellsFixture();
verifySpannedCellsFixtureStart();

toggleHideColumnAPI(0);

verifyNumberOfCellsInHeader(0, 7);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 6);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 2, CAPTION_3_4_5);
verifyHeaderCellContent(2, 0, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 1);
verifyHeaderCellColspan(1, 2, 3);
verifyHeaderCellColspan(2, 1, 2);

toggleHideColumnAPI(0);

verifySpannedCellsFixtureStart();

toggleHideColumnAPI(1);

verifyNumberOfCellsInHeader(0, 7);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 7);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 2, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 1);
verifyHeaderCellColspan(1, 2, 3);
verifyHeaderCellColspan(2, 1, 1);

toggleHideColumnAPI(3);

verifyNumberOfCellsInHeader(0, 6);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 6);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 2, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 1);
verifyHeaderCellColspan(1, 2, 2);
verifyHeaderCellColspan(2, 1, 1);

toggleHideColumnAPI(1);

verifyNumberOfCellsInHeader(0, 7);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 6);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 3, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 2);
verifyHeaderCellColspan(1, 3, 2);
verifyHeaderCellColspan(2, 1, 2);

toggleHideColumnAPI(3);

verifySpannedCellsFixtureStart();
}

@Test
public void testSpannedCells_hidingColumnInMiddle_rendersSpannedCellCorrectly() {
loadSpannedCellsFixture();
verifySpannedCellsFixtureStart();

toggleHideColumnAPI(4);

verifyNumberOfCellsInHeader(0, 7);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 6);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 3, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 2);
verifyHeaderCellColspan(1, 3, 2);
verifyHeaderCellColspan(2, 1, 2);

toggleHideColumnAPI(4);

verifySpannedCellsFixtureStart();
}

@Test
public void testSpannedCells_hidingColumnInEnd_rendersSpannedCellCorrectly() {
loadSpannedCellsFixture();
verifySpannedCellsFixtureStart();

toggleHideColumnAPI(1);

verifyNumberOfCellsInHeader(0, 7);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 7);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 2, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 1, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 1);
verifyHeaderCellColspan(1, 2, 3);
verifyHeaderCellColspan(2, 1, 1);

toggleHideColumnAPI(1);

verifySpannedCellsFixtureStart();

toggleHideColumnAPI(2);

verifyNumberOfCellsInHeader(0, 7);
verifyNumberOfCellsInHeader(1, 4);
verifyNumberOfCellsInHeader(2, 7);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 3, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 2);
verifyHeaderCellColspan(1, 3, 3);
verifyHeaderCellColspan(2, 1, 1);

toggleHideColumnAPI(5);

verifyNumberOfCellsInHeader(0, 6);
verifyNumberOfCellsInHeader(1, 4);
verifyNumberOfCellsInHeader(2, 6);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 3, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 2);
verifyHeaderCellColspan(1, 3, 2);
verifyHeaderCellColspan(2, 1, 1);

toggleHideColumnAPI(5);
toggleHideColumnAPI(2);

verifySpannedCellsFixtureStart();
}

@Test
public void testSpannedCells_spanningCellOverHiddenColumn_rendersSpannedCellCorrectly() {
selectMenuPath("Component", "State", "Width", "1000px");
appendHeaderRow();
toggleHideColumnAPI(4);
toggleHideColumnAPI(8);
toggleHideColumnAPI(9);
toggleHideColumnAPI(10);
toggleHideColumnAPI(11);
assertColumnHeaderOrder(0, 1, 2, 3, 5, 6, 7);
verifyNumberOfCellsInHeader(1, 7);

mergeHeaderCellsTwoThreeFour(2);

verifyNumberOfCellsInHeader(1, 6);
verifyHeaderCellContent(1, 3, CAPTION_3_4_5);
verifyHeaderCellColspan(1, 3, 2);
}

@Test
public void testSpannedCells_spanningCellAllHiddenColumns_rendersSpannedCellCorrectly() {
selectMenuPath("Component", "State", "Width", "1000px");
appendHeaderRow();
toggleHideColumnAPI(3);
toggleHideColumnAPI(4);
toggleHideColumnAPI(5);
toggleHideColumnAPI(8);
toggleHideColumnAPI(9);
toggleHideColumnAPI(10);
toggleHideColumnAPI(11);
assertColumnHeaderOrder(0, 1, 2, 6, 7);
verifyNumberOfCellsInHeader(1, 5);

mergeHeaderCellsTwoThreeFour(2);

verifyNumberOfCellsInHeader(1, 5);
verifyHeaderCellColspan(1, 0, 1);
verifyHeaderCellColspan(1, 1, 1);
verifyHeaderCellColspan(1, 2, 1);
verifyHeaderCellColspan(1, 3, 1);
verifyHeaderCellColspan(1, 4, 1);
}

private void loadSpannedCellsFixture() {
selectMenuPath("Component", "State", "Width", "1000px");
appendHeaderRow();
appendHeaderRow();
appendHeaderRow();
mergeHeaderCellsTwoThreeFour(2);
mergeHeaderCellsZeroOne(2);
mergeHeaderCellsOneTwo(3);
mergeHeaderCellsAll(4);
toggleHideColumnAPI(8);
toggleHideColumnAPI(9);
toggleHideColumnAPI(10);
toggleHideColumnAPI(11);
}

private void verifySpannedCellsFixtureStart() {
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6, 7);
verifyNumberOfCellsInHeader(0, 8);
verifyNumberOfCellsInHeader(1, 5);
verifyNumberOfCellsInHeader(2, 7);
verifyNumberOfCellsInHeader(3, 1);
verifyHeaderCellContent(1, 0, CAPTION_0_1);
verifyHeaderCellContent(1, 3, CAPTION_3_4_5);
verifyHeaderCellContent(2, 1, CAPTION_1_2);
verifyHeaderCellContent(3, 0, CAPTION_ALL);
verifyHeaderCellColspan(1, 0, 2);
verifyHeaderCellColspan(1, 3, 3);
verifyHeaderCellColspan(2, 1, 2);
}

private void toggleFrozenColumns(int count) {
selectMenuPath("Component", "State", "Frozen column count", count
+ " columns");
}

private void verifyHeaderCellColspan(int row, int column, int colspan) {
assertEquals(Integer.valueOf(colspan), Integer.valueOf(Integer
.parseInt(getGridElement().getHeaderCell(row, column)
.getAttribute("colspan"))));
}

private void verifyNumberOfCellsInHeader(int row, int numberOfCells) {
int size = 0;
for (TestBenchElement cell : getGridElement().getHeaderCells(row)) {
if (cell.isDisplayed()) {
size++;
}
}
assertEquals(numberOfCells, size);
}

private void verifyHeaderCellContent(int row, int column, String content) {
GridCellElement headerCell = getGridElement()
.getHeaderCell(row, column);
assertEquals(content.toLowerCase(), headerCell.getText().toLowerCase());
assertTrue(headerCell.isDisplayed());
}

private void verifyColumnIsFrozen(int index) {
assertTrue(getGridElement().getHeaderCell(0, index).isFrozen());
}

private void verifyColumnIsNotFrozen(int index) {
assertFalse(getGridElement().getHeaderCell(0, index).isFrozen());
}

private void verifyColumnHidingTogglesOrder(int... indices) {
WebElement sidebar = getSidebar();
List<WebElement> elements = sidebar.findElements(By
.className("column-hiding-toggle"));
for (int i = 0; i < indices.length; i++) {
WebElement e = elements.get(i);
assertTrue(("Header (0," + indices[i] + ")").equalsIgnoreCase(e
.getText()));
}
}

private void verifyColumnHidingOption(int columnIndex, boolean hidden) {
WebElement columnHidingToggle = getColumnHidingToggle(columnIndex);
assertEquals(hidden,
columnHidingToggle.getAttribute("class").contains("hidden"));
}

private void verifySidebarOpened() {
WebElement sidebar = getSidebar();
assertTrue(sidebar.getAttribute("class").contains("opened"));
}

private void verifySidebarClosed() {
WebElement sidebar = getSidebar();
assertFalse(sidebar.getAttribute("class").contains("opened"));
}

private void verifySidebarNotVisible() {
WebElement sidebar = getSidebar();
assertNull(sidebar);
}

private void verifySidebarVisible() {
WebElement sidebar = getSidebar();
assertNotNull(sidebar);
}

@Override
protected WebElement getSidebar() {
List<WebElement> elements = findElements(By.className("v-grid-sidebar"));
return elements.isEmpty() ? null : elements.get(0);
}

@Override
protected WebElement getSidebarOpenButton() {
List<WebElement> elements = findElements(By
.className("v-grid-sidebar-button"));
return elements.isEmpty() ? null : elements.get(0);
}

/**
* Returns the toggle inside the sidebar for hiding the column at the given
* index, or null if not found.
*/
@Override
protected WebElement getColumnHidingToggle(int columnIndex) {
WebElement sidebar = getSidebar();
List<WebElement> elements = sidebar.findElements(By
.className("column-hiding-toggle"));
for (WebElement e : elements) {
if (("Header (0," + columnIndex + ")")
.equalsIgnoreCase(e.getText())) {
return e;
}
}
return null;
}

private void clickSidebarOpenButton() {
getSidebarOpenButton().click();
}

private void moveColumnLeft(int index) {
selectMenuPath("Component", "Columns", "Column " + index,
"Move column left");
}

private void toggleHidableColumnAPI(int columnIndex) {
selectMenuPath("Component", "Columns", "Column " + columnIndex,
"Hidable");
}

private void toggleHideColumnAPI(int columnIndex) {
selectMenuPath("Component", "Columns", "Column " + columnIndex,
"Hidden");
}

private void appendHeaderRow() {
selectMenuPath("Component", "Header", "Append row");
}

private void mergeHeaderCellsZeroOne(int row) {
selectMenuPath("Component", "Header", "Row " + row, CAPTION_0_1);
}

private void mergeHeaderCellsOneTwo(int row) {
selectMenuPath("Component", "Header", "Row " + row, CAPTION_1_2);
}

private void mergeHeaderCellsTwoThreeFour(int row) {
selectMenuPath("Component", "Header", "Row " + row, CAPTION_3_4_5);
}

private void mergeHeaderCellsAll(int row) {
selectMenuPath("Component", "Header", "Row " + row, CAPTION_ALL);
}

}

+ 649
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java View File

@@ -0,0 +1,649 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.assertTrue;

import org.junit.Before;
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.testbench.parallel.TestCategory;

/**
*
* @author Vaadin Ltd
*/
@TestCategory("grid")
public class GridColumnReorderTest extends GridBasicClientFeaturesTest {

@Before
public void before() {
openTestURL();
}

@Test
public void columnReorderEventTriggered() {
final int firstIndex = 3;
final int secondIndex = 4;
final String firstHeaderText = getGridElement().getHeaderCell(0,
firstIndex).getText();
final String secondHeaderText = getGridElement().getHeaderCell(0,
secondIndex).getText();
selectMenuPath("Component", "Internals", "Listeners",
"Add ColumnReorder listener");
selectMenuPath("Component", "Columns", "Column " + secondIndex,
"Move column left");
// columns 3 and 4 should have swapped to 4 and 3
GridCellElement headerCell = getGridElement().getHeaderCell(0,
firstIndex);
assertEquals(secondHeaderText, headerCell.getText());
headerCell = getGridElement().getHeaderCell(0, secondIndex);
assertEquals(firstHeaderText, headerCell.getText());

// the reorder event should have typed the order to this label
WebElement columnReorderElement = findElement(By.id("columnreorder"));
int eventIndex = Integer.parseInt(columnReorderElement
.getAttribute("columns"));
assertEquals(1, eventIndex);

// trigger another event
selectMenuPath("Component", "Columns", "Column " + secondIndex,
"Move column left");
columnReorderElement = findElement(By.id("columnreorder"));
eventIndex = Integer.parseInt(columnReorderElement
.getAttribute("columns"));
assertEquals(2, eventIndex);
}

@Test
public void testColumnReorder_onReorder_columnReorderEventTriggered() {
final int firstIndex = 3;
final int secondIndex = 4;
final String firstHeaderText = getGridElement().getHeaderCell(0,
firstIndex).getText();
final String secondHeaderText = getGridElement().getHeaderCell(0,
secondIndex).getText();
selectMenuPath("Component", "Internals", "Listeners",
"Add ColumnReorder listener");
selectMenuPath("Component", "Columns", "Column " + secondIndex,
"Move column left");
// columns 3 and 4 should have swapped to 4 and 3
GridCellElement headerCell = getGridElement().getHeaderCell(0,
firstIndex);
assertEquals(secondHeaderText, headerCell.getText());
headerCell = getGridElement().getHeaderCell(0, secondIndex);
assertEquals(firstHeaderText, headerCell.getText());

// the reorder event should have typed the order to this label
WebElement columnReorderElement = findElement(By.id("columnreorder"));
int eventIndex = Integer.parseInt(columnReorderElement
.getAttribute("columns"));
assertEquals(1, eventIndex);

// trigger another event
selectMenuPath("Component", "Columns", "Column " + secondIndex,
"Move column left");
columnReorderElement = findElement(By.id("columnreorder"));
eventIndex = Integer.parseInt(columnReorderElement
.getAttribute("columns"));
assertEquals(2, eventIndex);
}

@Test
public void testColumnReorder_draggingSortedColumn_sortIndicatorShownOnDraggedElement() {
// given
toggleColumnReorder();
toggleSortableColumn(0);
sortColumn(0);

// when
startDragButDontDropOnDefaultColumnHeader(0);

// then
WebElement draggedElement = getDraggedHeaderElement();
assertTrue(draggedElement.getAttribute("class").contains("sort"));
}

@Test
public void testColumnReorder_draggingSortedColumn_sortStays() {
// given
toggleColumnReorder();
toggleSortableColumn(0);
sortColumn(0);

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);

// then
assertColumnIsSorted(1);
}

@Test
public void testColumnReorder_draggingFocusedHeader_focusShownOnDraggedElement() {
// given
toggleColumnReorder();
focusDefaultHeader(0);

// when
startDragButDontDropOnDefaultColumnHeader(0);

// then
WebElement draggedElement = getDraggedHeaderElement();
assertTrue(draggedElement.getAttribute("class").contains("focused"));
}

@Test
public void testColumnReorder_draggingFocusedHeader_focusIsKeptOnHeader() {
// given
toggleColumnReorder();
focusDefaultHeader(0);

// when
dragAndDropDefaultColumnHeader(0, 3, CellSide.LEFT);

// then
WebElement defaultColumnHeader = getDefaultColumnHeader(2);
String attribute = defaultColumnHeader.getAttribute("class");
assertTrue(attribute.contains("focused"));
}

@Test
public void testColumnReorder_draggingFocusedCellColumn_focusIsKeptOnCell() {
// given
toggleColumnReorder();
focusCell(2, 2);

// when
dragAndDropDefaultColumnHeader(2, 0, CellSide.LEFT);

// then
assertFocusedCell(2, 0);
}

@Test
public void testColumnReorderWithHiddenColumn_draggingFocusedCellColumnOverHiddenColumn_focusIsKeptOnCell() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Columns", "Column 1", "Hidden");
focusCell(2, 2);
assertFocusedCell(2, 2);

// when
dragAndDropDefaultColumnHeader(1, 0, CellSide.LEFT);

// then
assertFocusedCell(2, 2);

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);

// then
assertFocusedCell(2, 2);
}

@Test
public void testColumnReorder_dragColumnFromRightToLeftOfFocusedCellColumn_focusIsKept() {
// given
toggleColumnReorder();
focusCell(1, 3);

// when
dragAndDropDefaultColumnHeader(4, 1, CellSide.LEFT);

// then
assertFocusedCell(1, 4);
}

@Test
public void testColumnReorder_dragColumnFromLeftToRightOfFocusedCellColumn_focusIsKept() {
// given
toggleColumnReorder();
focusCell(4, 2);

// when
dragAndDropDefaultColumnHeader(0, 4, CellSide.LEFT);

// then
assertFocusedCell(4, 1);
}

@Test
public void testColumnReorder_draggingHeaderRowThatHasColumnHeadersSpanned_cantDropInsideSpannedHeaderFromOutside() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
int horizontalOffset = (getGridElement().getHeaderCell(1, 1).getSize()
.getWidth() / 2) - 10;
dragAndDropColumnHeader(1, 3, 1, horizontalOffset);

// then
assertColumnHeaderOrder(0, 3, 1, 2, 4);
}

@Test
public void testColumnReorder_anotherRowHasColumnHeadersSpanned_cantDropInsideSpannedHeaderFromOutside() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
int horizontalOffset = (getGridElement().getHeaderCell(1, 1).getSize()
.getWidth() / 2) + 10;
dragAndDropColumnHeader(0, 0, 2, horizontalOffset);

// then
assertColumnHeaderOrder(1, 2, 0, 3, 4);
}

@Test
public void testColumnReorder_cellInsideSpannedHeader_cantBeDroppedOutsideSpannedArea() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(0, 2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(0, 2, 1, 3, 4);
}

@Test
public void testColumnReorder_cellInsideTwoCrossingSpanningHeaders_cantTouchThis() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
dragAndDropColumnHeader(0, 3, 0, CellSide.LEFT);
assertColumnHeaderOrder(3, 0, 1, 2, 4);

// when
dragAndDropColumnHeader(0, 2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(3, 0, 1, 2, 4);
}

@Test
public void testColumnReorder_cellsInsideSpannedHeaderAndBlockedByOtherSpannedCells_cantTouchThose() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
dragAndDropColumnHeader(0, 3, 0, CellSide.LEFT);
assertColumnHeaderOrder(3, 0, 1, 2, 4);

// when then
dragAndDropColumnHeader(0, 1, 3, CellSide.LEFT);
assertColumnHeaderOrder(3, 0, 1, 2, 4);

dragAndDropColumnHeader(1, 2, 1, CellSide.LEFT);
assertColumnHeaderOrder(3, 0, 1, 2, 4);

dragAndDropColumnHeader(2, 1, 2, CellSide.RIGHT);
assertColumnHeaderOrder(3, 0, 1, 2, 4);
}

@Test
public void testColumnReorder_cellsInsideSpannedHeaderAndBlockedByOtherSpannedCells_reorderingLimited() {
// given
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
dragAndDropColumnHeader(0, 0, 4, CellSide.RIGHT);
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5);

// when then
dragAndDropColumnHeader(0, 1, 4, CellSide.LEFT);
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5);

dragAndDropColumnHeader(0, 2, 4, CellSide.LEFT);
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5);

dragAndDropColumnHeader(0, 3, 4, CellSide.RIGHT);
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 5, 4);

dragAndDropColumnHeader(0, 4, 2, CellSide.RIGHT);
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5);

dragAndDropColumnHeader(2, 3, 4, CellSide.RIGHT);
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 5, 4);

dragAndDropColumnHeader(2, 4, 2, CellSide.RIGHT);
scrollGridHorizontallyTo(0);
assertColumnHeaderOrder(1, 2, 3, 4, 5);
}

@Test
public void testColumnReorder_cellsInsideTwoAdjacentSpannedHeaders_reorderingLimited() {
// given
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
dragAndDropColumnHeader(0, 0, 4, CellSide.RIGHT);
scrollGridHorizontallyTo(0);
dragAndDropColumnHeader(0, 1, 4, CellSide.RIGHT);
scrollGridHorizontallyTo(0);
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
assertColumnHeaderOrder(1, 3, 4, 5, 2);

// when then
dragAndDropColumnHeader(0, 1, 4, CellSide.LEFT);
assertColumnHeaderOrder(1, 4, 3, 5, 2);

dragAndDropColumnHeader(0, 2, 4, CellSide.LEFT);
assertColumnHeaderOrder(1, 4, 3, 5, 2);

dragAndDropColumnHeader(0, 2, 0, CellSide.LEFT);
assertColumnHeaderOrder(1, 3, 4, 5, 2);
}

@Test
public void testColumnReorder_footerHasSpannedCells_cantDropInside() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Footer", "Append row");
selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(0, 3, 1, CellSide.RIGHT);

// then
assertColumnHeaderOrder(0, 3, 1, 2, 4);
}

@Test
public void testColumnReorder_cellInsideASpannedFooter_cantBeDroppedOutsideSpannedArea() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Footer", "Append row");
selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(0, 2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(0, 2, 1, 3, 4);
}

@Test
public void testColumnReorder_cellInsideTwoCrossingSpanningFooters_cantTouchThis() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Footer", "Append row");
selectMenuPath("Component", "Footer", "Append row");
selectMenuPath("Component", "Footer", "Row 1", "Join column cells 0, 1");
selectMenuPath("Component", "Footer", "Row 2", "Join columns 1, 2");
dragAndDropColumnHeader(0, 3, 0, CellSide.LEFT);
assertColumnHeaderOrder(3, 0, 1, 2, 4);

// when
dragAndDropColumnHeader(0, 2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(3, 0, 1, 2, 4);
}

@Test
public void testColumnReorder_cellsInsideTwoAdjacentSpannedHeaderAndFooter_reorderingLimited() {
// given
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Footer", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
dragAndDropColumnHeader(0, 0, 5, CellSide.LEFT);
scrollGridHorizontallyTo(0);
dragAndDropColumnHeader(0, 1, 5, CellSide.LEFT);
scrollGridHorizontallyTo(0);
selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2");
assertColumnHeaderOrder(1, 3, 4, 5, 2);

// when then
dragAndDropColumnHeader(0, 1, 3, CellSide.RIGHT);
assertColumnHeaderOrder(1, 4, 3, 5, 2);

dragAndDropColumnHeader(0, 2, 4, CellSide.RIGHT);
assertColumnHeaderOrder(1, 4, 3, 5, 2);

dragAndDropColumnHeader(0, 2, 0, CellSide.RIGHT);
assertColumnHeaderOrder(1, 3, 4, 5, 2);
}

@Test
public void testColumnReorder_draggingASpannedCell_dragWorksNormally() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(1, 1, 4, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 3, 1, 2, 4);
}

@Test
public void testColumnReorder_twoEqualSpannedCells_bothCanBeDragged() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(1, 1, 4, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 3, 1, 2, 4);

// when
dragAndDropColumnHeader(2, 3, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(1, 2, 0, 3, 4);
}

@Test
public void testColumReorder_twoCrossingSpanningHeaders_neitherCanBeDragged() {
// given
toggleColumnReorder();
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 3", "Join column cells 0, 1");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(1, 1, 4, CellSide.LEFT);

// then
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(2, 0, 3, CellSide.RIGHT);

// then
assertColumnHeaderOrder(0, 1, 2, 3, 4);
}

@Test
public void testColumnReorder_spannedCellHasAnotherSpannedCellInside_canBeDraggedNormally() {
// given
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
dragAndDropColumnHeader(1, 3, 1, CellSide.LEFT);
scrollGridHorizontallyTo(0);
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
assertColumnHeaderOrder(0, 3, 4, 5, 1);

// when
dragAndDropColumnHeader(1, 1, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(3, 4, 5, 0, 1);
}

@Test
public void testColumnReorder_spannedCellInsideAnotherSpanned_canBeDraggedWithBoundaries() {
// given
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
dragAndDropColumnHeader(1, 3, 1, CellSide.LEFT);
scrollGridHorizontallyTo(0);
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
assertColumnHeaderOrder(0, 3, 4, 5, 1);

// when
dragAndDropColumnHeader(2, 1, 3, CellSide.RIGHT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 5, 3, 4, 1);

// when
dragAndDropColumnHeader(2, 2, 0, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 3, 4, 5, 1);
}

@Test
public void testColumnReorder_cellInsideAndNextToSpannedCells_canBeDraggedWithBoundaries() {
// given
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
dragAndDropColumnHeader(1, 3, 1, CellSide.LEFT);
scrollGridHorizontallyTo(0);
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
assertColumnHeaderOrder(0, 3, 4, 5, 1);

// when
dragAndDropColumnHeader(2, 3, 0, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 5, 3, 4, 1);

// when
dragAndDropColumnHeader(2, 1, 4, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 3, 4, 5, 1);
}

@Test
public void testColumnReorder_multipleSpannedCells_dragWorksNormally() {
toggleColumnReorder();
selectMenuPath("Component", "State", "Width", "750px");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
selectMenuPath("Component", "Header", "Append row");
selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2");
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(1, 3, 1, CellSide.RIGHT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 3, 4, 5, 1);

// when
scrollGridHorizontallyTo(100);
dragAndDropColumnHeader(2, 4, 2, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(0, 1, 2, 3, 4);

// when
dragAndDropColumnHeader(0, 0, 3, CellSide.LEFT);
scrollGridHorizontallyTo(0);

// then
assertColumnHeaderOrder(1, 2, 0, 3, 4);
}

private void toggleSortableColumn(int index) {
selectMenuPath("Component", "Columns", "Column " + index, "Sortable");
}

private void startDragButDontDropOnDefaultColumnHeader(int index) {
new Actions(getDriver())
.clickAndHold(getGridHeaderRowCells().get(index))
.moveByOffset(100, 0).perform();
}

private void sortColumn(int index) {
getGridHeaderRowCells().get(index).click();
}

private void focusDefaultHeader(int index) {
getGridHeaderRowCells().get(index).click();
}

private WebElement getDraggedHeaderElement() {
return findElement(By.className("dragged-column-header"));
}
}

+ 24
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridSidebarFeatures.java View File

@@ -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.basicfeatures;

public class GridSidebarFeatures extends GridBasicFeatures {

@Override
protected boolean isColumnHidableByDefault(int col) {
return true;
}
}

+ 241
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java View File

@@ -0,0 +1,241 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;

import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.testbench.By;
import com.vaadin.testbench.ElementQuery;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.NotificationElement;
import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;

public class GridDetailsClientTest extends GridBasicClientFeaturesTest {

private static final String[] SET_GENERATOR = new String[] { "Component",
"Row details", "Set generator" };
private static final String[] SET_FAULTY_GENERATOR = new String[] {
"Component", "Row details", "Set faulty generator" };
private static final String[] SET_EMPTY_GENERATOR = new String[] {
"Component", "Row details", "Set empty generator" };

@Before
public void setUp() {
setDebug(true);
openTestURL();
}

@Test(expected = NoSuchElementException.class)
public void noDetailsByDefault() {
assertNull("details for row 1 should not exist at the start",
getGridElement().getDetails(1));
}

@Test
public void nullRendererShowsDetailsPlaceholder() {
toggleDetailsFor(1);
TestBenchElement details = getGridElement().getDetails(1);
assertNotNull("details for row 1 should not exist at the start",
details);
assertTrue("details should've been empty for null renderer", details
.getText().isEmpty());
}

@Test
public void applyRendererThenOpenDetails() {
selectMenuPath(SET_GENERATOR);
toggleDetailsFor(1);

TestBenchElement details = getGridElement().getDetails(1);
assertTrue("Unexpected details content",
details.getText().startsWith("Row: 1."));
}

@Test
public void openDetailsThenAppyRenderer() {
toggleDetailsFor(1);
selectMenuPath(SET_GENERATOR);

TestBenchElement details = getGridElement().getDetails(1);
assertTrue("Unexpected details content",
details.getText().startsWith("Row: 1."));
}

@Test
public void openHiddenDetailsThenScrollToIt() {
try {
getGridElement().getDetails(100);
fail("details row for 100 was apparently found, while it shouldn't have been.");
} catch (NoSuchElementException e) {
// expected
}

selectMenuPath(SET_GENERATOR);
toggleDetailsFor(100);

// scroll a bit beyond so we see below.
getGridElement().scrollToRow(101);

TestBenchElement details = getGridElement().getDetails(100);
assertTrue("Unexpected details content",
details.getText().startsWith("Row: 100."));
}

@Test
public void errorUpdaterShowsErrorNotification() {
assertFalse("No notifications should've been at the start",
$(NotificationElement.class).exists());

toggleDetailsFor(1);
selectMenuPath(SET_FAULTY_GENERATOR);

ElementQuery<NotificationElement> notification = $(NotificationElement.class);
assertTrue("Was expecting an error notification here",
notification.exists());
notification.first().close();

assertEquals("The error details element should be empty", "",
getGridElement().getDetails(1).getText());
}

@Test
public void updaterStillWorksAfterError() {
toggleDetailsFor(1);

selectMenuPath(SET_FAULTY_GENERATOR);
$(NotificationElement.class).first().close();
selectMenuPath(SET_GENERATOR);

assertNotEquals(
"New details should've been generated even after error", "",
getGridElement().getDetails(1).getText());
}

@Test
public void updaterRendersExpectedWidgets() {
selectMenuPath(SET_GENERATOR);
toggleDetailsFor(1);

TestBenchElement detailsElement = getGridElement().getDetails(1);
assertNotNull(detailsElement.findElement(By.className("gwt-Label")));
assertNotNull(detailsElement.findElement(By.className("gwt-Button")));
}

@Test
public void widgetsInUpdaterWorkAsExpected() {
selectMenuPath(SET_GENERATOR);
toggleDetailsFor(1);

TestBenchElement detailsElement = getGridElement().getDetails(1);
WebElement button = detailsElement.findElement(By
.className("gwt-Button"));
button.click();

WebElement label = detailsElement
.findElement(By.className("gwt-Label"));
assertEquals("clicked", label.getText());
}

@Test
public void emptyGenerator() {
selectMenuPath(SET_EMPTY_GENERATOR);
toggleDetailsFor(1);

assertEquals("empty generator did not produce an empty details row",
"", getGridElement().getDetails(1).getText());
}

@Test(expected = NoSuchElementException.class)
public void removeDetailsRow() {
selectMenuPath(SET_GENERATOR);
toggleDetailsFor(1);
toggleDetailsFor(1);

getGridElement().getDetails(1);
}

@Test
public void rowElementClassNames() {
toggleDetailsFor(0);
toggleDetailsFor(1);

List<WebElement> elements = getGridElement().findElements(
By.className("v-grid-spacer"));
assertEquals("v-grid-spacer", elements.get(0).getAttribute("class"));
assertEquals("v-grid-spacer stripe",
elements.get(1).getAttribute("class"));
}

@Test
public void scrollDownToRowWithDetails() {
toggleDetailsFor(100);
scrollToRow(100, ScrollDestination.ANY);

Range validScrollRange = Range.between(1700, 1715);
assertTrue(validScrollRange.contains(getGridVerticalScrollPos()));
}

@Test
public void scrollUpToRowWithDetails() {
toggleDetailsFor(100);
scrollGridVerticallyTo(999999);
scrollToRow(100, ScrollDestination.ANY);

Range validScrollRange = Range.between(1990, 2010);
assertTrue(validScrollRange.contains(getGridVerticalScrollPos()));
}

@Test
public void cannotScrollBeforeTop() {
toggleDetailsFor(1);
scrollToRow(0, ScrollDestination.END);
assertEquals(0, getGridVerticalScrollPos());
}

@Test
public void cannotScrollAfterBottom() {
toggleDetailsFor(999);
scrollToRow(999, ScrollDestination.START);

Range expectedRange = Range.withLength(19680, 20);
assertTrue(expectedRange.contains(getGridVerticalScrollPos()));
}

private void scrollToRow(int rowIndex, ScrollDestination destination) {
selectMenuPath(new String[] { "Component", "State", "Scroll to...",
"Row " + rowIndex + "...", "Destination " + destination });
}

private void toggleDetailsFor(int rowIndex) {
selectMenuPath(new String[] { "Component", "Row details",
"Toggle details for...", "Row " + rowIndex });
}
}

+ 156
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridSidebarContentTest.java View File

@@ -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.tests.components.grid.basicfeatures.client;

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.components.grid.basicfeatures.GridBasicClientFeaturesTest;
import com.vaadin.tests.components.grid.basicfeatures.element.CustomGridElement;

public class GridSidebarContentTest extends GridBasicClientFeaturesTest {

@Test
public void testSidebarWithHidableColumn() {
openTestURL();
CustomGridElement gridElement = getGridElement();

Assert.assertEquals("Sidebar should not be initially present", 0,
countBySelector(".v-grid-sidebar"));

selectMenuPath("Component", "Columns", "Column 0", "Hidable");

gridElement.findElement(By.className("v-grid-sidebar-button")).click();

WebElement toggle = gridElement.findElement(By
.className("column-hiding-toggle"));

Assert.assertEquals("Column 0 should be togglable", "Header (0,0)",
toggle.getText());

selectMenuPath("Component", "Columns", "Column 0", "Hidable");
Assert.assertEquals("Sidebar should disappear without toggable column",
0, countBySelector(".v-grid-sidebar"));

}

@Test
public void testAddingCustomSidebarItem() {
openTestURL();
CustomGridElement gridElement = getGridElement();

selectMenuPath("Component", "Sidebar", "Add item to end");

gridElement.findElement(By.className("v-grid-sidebar-button")).click();

WebElement sidebarItem = gridElement.findElement(By
.cssSelector(".v-grid-sidebar-content .gwt-MenuItem"));

sidebarItem.click();

Assert.assertEquals("Sidebar should be closed after clicking item 0",
0, countBySelector(".v-grid-sidebar-content"));
}

@Test
public void testProgrammaticSidebarToggle() {
openTestURL();

selectMenuPath("Component", "Columns", "Column 0", "Hidable");

selectMenuPath("Component", "Sidebar", "Toggle sidebar visibility");

Assert.assertEquals("Sidebar be open", 1,
countBySelector(".v-grid-sidebar-content"));

selectMenuPath("Component", "Sidebar", "Toggle sidebar visibility");

Assert.assertEquals("Sidebar be closed", 0,
countBySelector(".v-grid-sidebar-content"));
}

@Test
public void testBasicSidebarOrder() {
openTestURL();
CustomGridElement gridElement = getGridElement();

// First add custom content
selectMenuPath("Component", "Sidebar", "Add separator to end");
selectMenuPath("Component", "Sidebar", "Add item to end");

// Then make one column togglable
selectMenuPath("Component", "Columns", "Column 0", "Hidable");

selectMenuPath("Component", "Sidebar", "Toggle sidebar visibility");

assertSidebarMenuItems("Header (0,0)", null, "Custom menu item 0");
}

@Test
public void testSidebarOrderAbuse() {
openTestURL();
CustomGridElement gridElement = getGridElement();

selectMenuPath("Component", "Columns", "Column 0", "Hidable");
selectMenuPath("Component", "Columns", "Column 1", "Hidable");

// Inserts a menu item between the two visibility toggles
selectMenuPath("Component", "Sidebar", "Add item before index 1");

selectMenuPath("Component", "Sidebar", "Toggle sidebar visibility");

// Total order enforcement not implemented at this point. Test can be
// updated when it is.
assertSidebarMenuItems("Header (0,0)", "Custom menu item 0",
"Header (0,1)");

selectMenuPath("Component", "Columns", "Column 2", "Hidable");

// Adding a new togglable column should have restored the expected order
assertSidebarMenuItems("Header (0,0)", "Header (0,1)", "Header (0,2)",
"Custom menu item 0");
}

private void assertSidebarMenuItems(String... items) {
List<WebElement> menuItems = getGridElement().findElements(
By.cssSelector(".v-grid-sidebar-content td"));

Assert.assertEquals("Expected " + items.length + " menu items",
items.length, menuItems.size());

for (int i = 0; i < items.length; i++) {
String expectedItem = items[i];
if (expectedItem == null) {
Assert.assertEquals("Item " + i + " should be a separator",
"gwt-MenuItemSeparator",
menuItems.get(i).getAttribute("class"));
} else {
Assert.assertEquals("Unexpected content for item " + i,
expectedItem, menuItems.get(i).getText());
}
}
}

private int countBySelector(String cssSelector) {
return getGridElement().findElements(By.cssSelector(cssSelector))
.size();
}

}

+ 46
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/element/CustomGridElement.java View File

@@ -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.basicfeatures.element;

import org.openqa.selenium.NoSuchElementException;

import com.vaadin.testbench.By;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.GridElement;
import com.vaadin.testbench.elementsbase.ServerClass;

@ServerClass("com.vaadin.ui.Grid")
public class CustomGridElement extends GridElement {
/**
* Gets the element that contains the details of a row.
*
* @since
* @param rowIndex
* the index of the row for the details
* @return the element that contains the details of a row. <code>null</code>
* if no widget is defined for the detials row
* @throws NoSuchElementException
* if the given details row is currently not open
*/
public TestBenchElement getDetails(int rowIndex)
throws NoSuchElementException {
return getSubPart("#details[" + rowIndex + "]");
}

private TestBenchElement getSubPart(String subPartSelector) {
return (TestBenchElement) findElement(By.vaadin(subPartSelector));
}
}

+ 2
- 4
uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java View File

@@ -66,10 +66,8 @@ public class EscalatorBasicsTest extends EscalatorBasicClientFeaturesTest {
selectMenuPath(GENERAL, DETACH_ESCALATOR);
selectMenuPath(GENERAL, ATTACH_ESCALATOR);

assertEquals("Vertical scroll position", "50", getVerticalScrollbar()
.getAttribute("scrollTop"));
assertEquals("Horizontal scroll position", "50",
getHorizontalScrollbar().getAttribute("scrollLeft"));
assertEquals("Vertical scroll position", 50, getScrollTop());
assertEquals("Horizontal scroll position", 50, getScrollLeft());

assertEquals("First cell of first visible row", "Row 2: 0,2",
getBodyCell(0, 0).getText());

+ 583
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java View File

@@ -0,0 +1,583 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Before;
import org.junit.ComparisonFailure;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;

import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.grid.Range;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.NotificationElement;
import com.vaadin.testbench.parallel.BrowserUtil;
import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;

@SuppressWarnings("boxing")
public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest {

//@formatter:off
// separate strings made so that eclipse can show the concatenated string by hovering the mouse over the constant
// translate3d(0px, 40px, 123px);
// translate3d(24px, 15.251px, 0);
// translate(0, 40px);
private final static String TRANSLATE_VALUE_REGEX =
"translate(?:3d|)" // "translate" or "translate3d"
+ "\\(" // literal "("
+ "(" // start capturing the x argument
+ "[0-9]+" // the integer part of the value
+ "(?:" // start of the subpixel part of the value
+ "\\.[0-9]" // if we have a period, there must be at least one number after it
+ "[0-9]*" // any amount of accuracy afterwards is fine
+ ")?" // the subpixel part is optional
+ ")"
+ "(?:px)?" // we don't care if the values are suffixed by "px" or not.
+ ", "
+ "(" // start capturing the y argument
+ "[0-9]+" // the integer part of the value
+ "(?:" // start of the subpixel part of the value
+ "\\.[0-9]" // if we have a period, there must be at least one number after it
+ "[0-9]*" // any amount of accuracy afterwards is fine
+ ")?" // the subpixel part is optional
+ ")"
+ "(?:px)?" // we don't care if the values are suffixed by "px" or not.
+ "(?:, .*?)?" // the possible z argument, uninteresting (translate doesn't have one, translate3d does)
+ "\\)" // literal ")"
+ ";?"; // optional ending semicolon
// 40px;
// 12.34px
private final static String PIXEL_VALUE_REGEX =
"(" // capture the pixel value
+ "[0-9]+" // the pixel argument
+ "(?:" // start of the subpixel part of the value
+ "\\.[0-9]" // if we have a period, there must be at least one number after it
+ "[0-9]*" // any amount of accuracy afterwards is fine
+ ")?" // the subpixel part is optional
+ ")"
+ "(?:px)?" // optional "px" string
+ ";?"; // optional semicolon
//@formatter:on

// also matches "-webkit-transform";
private final static Pattern TRANSFORM_CSS_PATTERN = Pattern
.compile("transform: (.*?);");
private final static Pattern TOP_CSS_PATTERN = Pattern.compile(
"top: ([0-9]+(?:\\.[0-9]+)?(?:px)?);?", Pattern.CASE_INSENSITIVE);
private final static Pattern LEFT_CSS_PATTERN = Pattern.compile(
"left: ([0-9]+(?:\\.[0-9]+)?(?:px)?);?", Pattern.CASE_INSENSITIVE);

private final static Pattern TRANSLATE_VALUE_PATTERN = Pattern
.compile(TRANSLATE_VALUE_REGEX);
private final static Pattern PIXEL_VALUE_PATTERN = Pattern.compile(
PIXEL_VALUE_REGEX, Pattern.CASE_INSENSITIVE);

@Before
public void before() {
setDebug(true);
openTestURL();
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, "Set 20px default height");
populate();
}

@Test
public void openVisibleSpacer() {
assertFalse("No spacers should be shown at the start",
spacersAreFoundInDom());
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertNotNull("Spacer should be shown after setting it", getSpacer(1));
}

@Test
public void closeVisibleSpacer() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_1, REMOVE);
assertNull("Spacer should not exist after removing it", getSpacer(1));
}

@Test
public void spacerPushesVisibleRowsDown() {
double oldTop = getElementTop(getBodyRow(2));
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double newTop = getElementTop(getBodyRow(2));

assertGreater("Row below a spacer was not pushed down", newTop, oldTop);
}

@Test
public void addingRowAboveSpacerPushesItDown() {
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);

selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double oldTop = getElementTop(getSpacer(1));
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
double newTop = getElementTop(getSpacer(2));

assertGreater("Spacer should've been pushed down (oldTop: " + oldTop
+ ", newTop: " + newTop + ")", newTop, oldTop);
}

@Test
public void addingRowBelowSpacerDoesNotPushItDown() {
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);

selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double oldTop = getElementTop(getSpacer(1));
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END);
double newTop = getElementTop(getSpacer(1));

assertEquals("Spacer should've not been pushed down", newTop, oldTop,
WidgetUtil.PIXEL_EPSILON);
}

@Test
public void addingRowBelowSpacerIsActuallyRenderedBelowWhenEscalatorIsEmpty() {
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);

selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double spacerTop = getElementTop(getSpacer(1));
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END);
double rowTop = getElementTop(getBodyRow(2));

assertEquals("Next row should've been rendered below the spacer",
spacerTop + 100, rowTop, WidgetUtil.PIXEL_EPSILON);
}

@Test
public void addSpacerAtBottomThenScrollThere() {
selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX);
scrollVerticallyTo(999999);

assertFalse("Did not expect a notification",
$(NotificationElement.class).exists());
}

@Test
public void scrollToBottomThenAddSpacerThere() {
scrollVerticallyTo(999999);
long oldBottomScrollTop = getScrollTop();
selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX);

assertEquals("Adding a spacer underneath the current viewport should "
+ "not scroll anywhere", oldBottomScrollTop, getScrollTop());
assertFalse("Got an unexpected notification",
$(NotificationElement.class).exists());

scrollVerticallyTo(999999);

assertFalse("Got an unexpected notification",
$(NotificationElement.class).exists());
assertGreater("Adding a spacer should've made the scrollbar scroll "
+ "further", getScrollTop(), oldBottomScrollTop);
}

@Test
public void removingRowAboveSpacerMovesSpacerUp() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
WebElement spacer = getSpacer(1);
double originalElementTop = getElementTop(spacer);

selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
REMOVE_ONE_ROW_FROM_BEGINNING);
assertLessThan("spacer should've moved up", getElementTop(spacer),
originalElementTop);
assertNull("No spacer for row 1 should be found after removing the "
+ "top row", getSpacer(1));
}

@Test
public void removingSpacedRowRemovesSpacer() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertTrue("Spacer should've been found in the DOM",
spacersAreFoundInDom());

selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
REMOVE_ONE_ROW_FROM_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
REMOVE_ONE_ROW_FROM_BEGINNING);

assertFalse("No spacers should be in the DOM after removing "
+ "associated spacer", spacersAreFoundInDom());

}

@Test
public void spacersAreFixedInViewport_firstFreezeThenScroll() {
selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertEquals("Spacer's left position should've been 0 at the "
+ "beginning", 0d, getElementLeft(getSpacer(1)),
WidgetUtil.PIXEL_EPSILON);

int scrollTo = 10;
scrollHorizontallyTo(scrollTo);
assertEquals("Spacer's left position should've been " + scrollTo
+ " after scrolling " + scrollTo + "px", scrollTo,
getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON);
}

@Test
public void spacersAreFixedInViewport_firstScrollThenFreeze() {
selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN);
int scrollTo = 10;
scrollHorizontallyTo(scrollTo);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertEquals("Spacer's left position should've been " + scrollTo
+ " after scrolling " + scrollTo + "px", scrollTo,
getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON);
}

@Test
public void addingMinusOneSpacerDoesNotScrollWhenScrolledAtTop() {
scrollVerticallyTo(5);
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX);
assertEquals(
"No scroll adjustment should've happened when adding the -1 spacer",
5, getScrollTop());
}

@Test
public void removingMinusOneSpacerScrolls() {
scrollVerticallyTo(5);
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, REMOVE);
assertEquals("Scroll adjustment should've happened when removing the "
+ "-1 spacer", 0, getScrollTop());
}

@Test
public void scrollToRowWorksProperlyWithSpacers() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);

/*
* we check for row -2 instead of -1, because escalator has the one row
* buffered underneath the footer
*/
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_75);
Thread.sleep(500);
assertEquals("Row 75: 0,75", getBodyCell(-2, 0).getText());

selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_25);
Thread.sleep(500);

try {
assertEquals("Row 25: 0,25", getBodyCell(0, 0).getText());
} catch (ComparisonFailure retryForIE10andIE11) {
/*
* This seems to be some kind of subpixel/off-by-one-pixel error.
* Everything's scrolled correctly, but Escalator still loads one
* row above to the DOM, underneath the header. It's there, but it's
* not visible. We'll allow for that one pixel error.
*/
assertEquals("Row 24: 0,24", getBodyCell(0, 0).getText());
}
}

@Test
public void scrollToSpacerFromAbove() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);

// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(765, 780);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}

@Test
public void scrollToSpacerFromBelow() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(999999);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);

// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(1015, 1025);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}

@Test
public void scrollToSpacerAlreadyInViewport() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(1000);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);

assertEquals(getScrollTop(), 1000);
}

@Test
public void scrollToRowAndSpacerFromAbove() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50,
SCROLL_HERE_SPACERBELOW_ANY_0PADDING);

// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(765, 780);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}

@Test
public void scrollToRowAndSpacerFromBelow() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(999999);
selectMenuPath(FEATURES, SPACERS, ROW_50,
SCROLL_HERE_SPACERBELOW_ANY_0PADDING);

// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(995, 1005);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}

@Test
public void scrollToRowAndSpacerAlreadyInViewport() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(950);
selectMenuPath(FEATURES, SPACERS, ROW_50,
SCROLL_HERE_SPACERBELOW_ANY_0PADDING);

assertEquals(getScrollTop(), 950);
}

@Test
public void domCanBeSortedWithFocusInSpacer() throws InterruptedException {

// Firefox behaves badly with focus-related tests - skip it.
if (BrowserUtil.isFirefox(super.getDesiredCapabilities())) {
return;
}

selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);

WebElement inputElement = getEscalator().findElement(
By.tagName("input"));
inputElement.click();
scrollVerticallyTo(30);

// Sleep needed because of all the JS we're doing, and to let
// the DOM reordering to take place.
Thread.sleep(500);

assertFalse("Error message detected", $(NotificationElement.class)
.exists());
}

@Test
public void spacersAreInsertedInCorrectDomPosition() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);

WebElement tbody = getEscalator().findElement(By.tagName("tbody"));
WebElement spacer = getChild(tbody, 2);
String cssClass = spacer.getAttribute("class");
assertTrue("element index 2 was not a spacer (class=\"" + cssClass
+ "\")", cssClass.contains("-spacer"));
}

@Test
public void spacersAreInCorrectDomPositionAfterScroll() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);

scrollVerticallyTo(32); // roughly one row's worth

WebElement tbody = getEscalator().findElement(By.tagName("tbody"));
WebElement spacer = getChild(tbody, 1);
String cssClass = spacer.getAttribute("class");
assertTrue("element index 1 was not a spacer (class=\"" + cssClass
+ "\")", cssClass.contains("-spacer"));
}

@Test
public void spacerScrolledIntoViewGetsFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);

tryToTabIntoFocusUpdaterElement();
assertEquals("input", getFocusedElement().getTagName());
}

@Test
public void spacerScrolledOutOfViewDoesNotGetFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);

tryToTabIntoFocusUpdaterElement();
assertNotEquals("input", getFocusedElement().getTagName());
}

@Test
public void spacerOpenedInViewGetsFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
tryToTabIntoFocusUpdaterElement();
WebElement focusedElement = getFocusedElement();
assertEquals("input", focusedElement.getTagName());
}

@Test
public void spacerOpenedOutOfViewDoesNotGetFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);

tryToTabIntoFocusUpdaterElement();
assertNotEquals("input", getFocusedElement().getTagName());
}

@Test
public void spacerOpenedInViewAndScrolledOutAndBackAgainGetsFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_50);
selectMenuPath(FEATURES, SPACERS, ROW_1, SCROLL_HERE_ANY_0PADDING);

tryToTabIntoFocusUpdaterElement();
assertEquals("input", getFocusedElement().getTagName());
}

@Test
public void spacerOpenedOutOfViewAndScrolledInAndBackAgainDoesNotGetFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_0);

tryToTabIntoFocusUpdaterElement();
assertNotEquals("input", getFocusedElement().getTagName());
}

private void tryToTabIntoFocusUpdaterElement() {
((TestBenchElement) findElement(By.className("gwt-MenuBar"))).focus();
WebElement focusedElement = getFocusedElement();
focusedElement.sendKeys(Keys.TAB);
}

private WebElement getChild(WebElement parent, int childIndex) {
return (WebElement) executeScript("return arguments[0].children["
+ childIndex + "];", parent);
}

private static double[] getElementDimensions(WebElement element) {
/*
* we need to parse the style attribute, since using getCssValue gets a
* normalized value that is harder to parse.
*/
String style = element.getAttribute("style");

String transform = getTransformFromStyle(style);
if (transform != null) {
return getTranslateValues(transform);
}

double[] result = new double[] { -1, -1 };
String left = getLeftFromStyle(style);
if (left != null) {
result[0] = getPixelValue(left);
}
String top = getTopFromStyle(style);
if (top != null) {
result[1] = getPixelValue(top);
}

if (result[0] != -1 && result[1] != -1) {
return result;
} else {
throw new IllegalArgumentException("Could not parse the position "
+ "information from the CSS \"" + style + "\"");
}
}

private static double getElementTop(WebElement element) {
return getElementDimensions(element)[1];
}

private static double getElementLeft(WebElement element) {
return getElementDimensions(element)[0];
}

private static String getTransformFromStyle(String style) {
return getFromStyle(TRANSFORM_CSS_PATTERN, style);
}

private static String getTopFromStyle(String style) {
return getFromStyle(TOP_CSS_PATTERN, style);
}

private static String getLeftFromStyle(String style) {
return getFromStyle(LEFT_CSS_PATTERN, style);
}

private static String getFromStyle(Pattern pattern, String style) {
Matcher matcher = pattern.matcher(style);
if (matcher.find()) {
assertEquals("wrong amount of groups matched in " + style, 1,
matcher.groupCount());
return matcher.group(1);
} else {
return null;
}
}

/**
* @return {@code [0] == x}, {@code [1] == y}
*/
private static double[] getTranslateValues(String translate) {
Matcher matcher = TRANSLATE_VALUE_PATTERN.matcher(translate);
assertTrue("no matches for " + translate + " against "
+ TRANSLATE_VALUE_PATTERN, matcher.find());
assertEquals("wrong amout of groups matched in " + translate, 2,
matcher.groupCount());

return new double[] { Double.parseDouble(matcher.group(1)),
Double.parseDouble(matcher.group(2)) };
}

private static double getPixelValue(String top) {
Matcher matcher = PIXEL_VALUE_PATTERN.matcher(top);
assertTrue("no matches for \"" + top + "\" against "
+ PIXEL_VALUE_PATTERN, matcher.find());
assertEquals("wrong amount of groups matched in " + top, 1,
matcher.groupCount());
return Double.parseDouble(matcher.group(1));
}
}

+ 347
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java View File

@@ -0,0 +1,347 @@
/*
* Copyright 2000-2014 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS 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 java.util.List;

import org.junit.Before;
import org.junit.Test;

import com.vaadin.testbench.TestBenchElement;
import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;

/**
* Tests that Grid columns can be reordered by user with drag and drop #16643.
*
* @author Vaadin Ltd
*/
public class GridColumnReorderTest extends GridBasicFeaturesTest {

private static final String[] COLUMN_REORDERING_PATH = { "Component",
"State", "Column Reordering Allowed" };
private static final String[] COLUMN_REORDER_LISTENER_PATH = { "Component",
"State", "ColumnReorderListener" };

@Before
public void setUp() {
setDebug(true);
}

@Test
public void testColumnReordering_firstColumnDroppedOnThird_dropOnLeftSide() {
// given
openTestURL();
assertColumnHeaderOrder(0, 1, 2);
toggleColumnReordering();

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);

// then
assertColumnHeaderOrder(1, 0, 2);
}

@Test
public void testColumnReordering_firstColumnDroppedOnThird_dropOnRightSide() {
// given
openTestURL();
assertColumnHeaderOrder(0, 1, 2);
toggleColumnReordering();

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.RIGHT);

// then
assertColumnHeaderOrder(1, 2, 0);
}

@Test
public void testColumnReordering_reorderingTwiceBackForth_reordered() {
// given
openTestURL();
selectMenuPath("Component", "Size", "Width", "800px");
assertColumnHeaderOrder(0, 1, 2, 3, 4);
toggleColumnReordering();

// when
dragAndDropDefaultColumnHeader(2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(2, 0, 1, 3, 4);

// when
dragAndDropDefaultColumnHeader(1, 3, CellSide.RIGHT);

// then
assertColumnHeaderOrder(2, 1, 3, 0);
}

@Test
public void testColumnReordering_notEnabled_noReordering() {
// given
openTestURL();
assertColumnHeaderOrder(0, 1, 2);

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.RIGHT);

// then
assertColumnHeaderOrder(0, 1, 2);
}

@Test
public void testColumnReordering_userChangesRevertedByServer_columnsAreUpdated() {
// given
openTestURL();
assertColumnHeaderOrder(0, 1, 2);
toggleColumnReordering();

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);
assertColumnHeaderOrder(1, 0, 2);
moveColumnManuallyLeftByOne(0);

// then
assertColumnHeaderOrder(0, 1, 2);
}

@Test
public void testColumnReordering_concurrentUpdatesFromServer_columnOrderFromServerUsed() {
// given
openTestURL();
assertColumnHeaderOrder(0, 1, 2);
toggleColumnReordering();

// when
selectMenuPath(new String[] { "Component", "Internals",
"Update column order without updating client" });
dragAndDropDefaultColumnHeader(2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(1, 0, 2);
}

@Test
public void testColumnReordering_triggersReorderEvent_isUserInitiated() {
// given
openTestURL();
toggleColumnReordering();

// when
toggleColumnReorderListener();
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);

// then
assertColumnReorderEvent(true);
}

@Test
public void testColumnReordering_addAndRemoveListener_registerUnRegisterWorks() {
// given
openTestURL();
toggleColumnReordering();
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);
assertNoColumnReorderEvent();

// when
toggleColumnReorderListener();
dragAndDropDefaultColumnHeader(0, 2, CellSide.RIGHT);

// then
assertColumnReorderEvent(true);

// when
toggleColumnReorderListener();
dragAndDropDefaultColumnHeader(0, 3, CellSide.LEFT);

// then
assertNoColumnReorderEvent();
}

@Test
public void testColumnReorderingEvent_serverSideReorder_triggersReorderEvent() {
openTestURL();

// when
toggleColumnReorderListener();
moveColumnManuallyLeftByOne(3);

// then
assertColumnReorderEvent(false);
}

@Test
public void testColumnReorder_draggingFrozenColumns_impossible() {
// given
openTestURL();
toggleColumnReordering();
setFrozenColumns(2);
assertColumnHeaderOrder(0, 1, 2, 3);

// when
dragAndDropDefaultColumnHeader(0, 2, CellSide.LEFT);

// then
assertColumnHeaderOrder(0, 1, 2, 3);
assertTrue(getGridElement().getHeaderCell(0, 0).isFrozen());
assertTrue(getGridElement().getHeaderCell(0, 1).isFrozen());
assertFalse(getGridElement().getHeaderCell(0, 2).isFrozen());
}

@Test
public void testColumnReorder_draggingColumnOnTopOfFrozenColumn_columnDroppedRightOfFrozenColumns() {
// given
openTestURL();
toggleColumnReordering();
setFrozenColumns(1);
assertColumnHeaderOrder(0, 1, 2, 3);

// when
dragAndDropDefaultColumnHeader(2, 0, CellSide.LEFT);

// then
assertColumnHeaderOrder(0, 2, 1, 3);
}

@Test
public void testColumnReorder_draggingColumnLeftOfMultiSelectionColumn_columnDroppedRight() {
// given
openTestURL();
toggleColumnReordering();
selectMenuPath("Component", "State", "Selection mode", "multi");
List<TestBenchElement> gridHeaderRowCells = getGridHeaderRowCells();
assertTrue(gridHeaderRowCells.get(0).getText().equals(""));
assertColumnHeader("Column 0", gridHeaderRowCells.get(1));
assertColumnHeader("Column 1", gridHeaderRowCells.get(2));
assertColumnHeader("Column 2", gridHeaderRowCells.get(3));

// when
dragAndDropDefaultColumnHeader(2, 0, CellSide.LEFT);

// then
gridHeaderRowCells = getGridHeaderRowCells();
assertTrue(gridHeaderRowCells.get(0).getText().equals(""));
assertColumnHeader("Column 1", gridHeaderRowCells.get(1));
assertColumnHeader("Column 0", gridHeaderRowCells.get(2));
assertColumnHeader("Column 2", gridHeaderRowCells.get(3));
}

@Test
public void testColumnReorder_multiSelectionAndFrozenColumns_columnDroppedRight() {
// given
openTestURL();
toggleColumnReordering();
selectMenuPath("Component", "State", "Selection mode", "multi");
setFrozenColumns(1);
List<TestBenchElement> gridHeaderRowCells = getGridHeaderRowCells();
assertTrue(gridHeaderRowCells.get(0).getText().equals(""));
assertColumnHeader("Column 0", gridHeaderRowCells.get(1));
assertColumnHeader("Column 1", gridHeaderRowCells.get(2));
assertColumnHeader("Column 2", gridHeaderRowCells.get(3));

// when
dragAndDropDefaultColumnHeader(3, 0, CellSide.LEFT);

// then
gridHeaderRowCells = getGridHeaderRowCells();
assertTrue(gridHeaderRowCells.get(0).getText().equals(""));
assertColumnHeader("Column 0", gridHeaderRowCells.get(1));
assertColumnHeader("Column 2", gridHeaderRowCells.get(2));
assertColumnHeader("Column 1", gridHeaderRowCells.get(3));
}

@Test
public void testColumnReordering_multiSelectionColumnNotFrozen_stillCantDropLeftSide() {
// given
openTestURL();
toggleColumnReordering();
selectMenuPath("Component", "State", "Selection mode", "multi");
setFrozenColumns(-1);
List<TestBenchElement> gridHeaderRowCells = getGridHeaderRowCells();
assertTrue(gridHeaderRowCells.get(0).getText().equals(""));
assertColumnHeader("Column 0", gridHeaderRowCells.get(1));
assertColumnHeader("Column 1", gridHeaderRowCells.get(2));
assertColumnHeader("Column 2", gridHeaderRowCells.get(3));

// when
dragAndDropDefaultColumnHeader(2, 0, CellSide.LEFT);

// then
gridHeaderRowCells = getGridHeaderRowCells();
assertTrue(gridHeaderRowCells.get(0).getText().equals(""));
assertColumnHeader("Column 1", gridHeaderRowCells.get(1));
assertColumnHeader("Column 0", gridHeaderRowCells.get(2));
assertColumnHeader("Column 2", gridHeaderRowCells.get(3));
}

@Test
public void testColumnReordering_twoHeaderRows_dndReorderingPossibleFromFirstRow() {
// given
openTestURL();
toggleColumnReordering();
selectMenuPath("Component", "Header", "Append row");
assertColumnHeaderOrder(0, 1, 2, 3);

// when
dragAndDropColumnHeader(0, 0, 2, CellSide.RIGHT);

// then
assertColumnHeaderOrder(1, 2, 0, 3);
}

@Test
public void testColumnReordering_twoHeaderRows_dndReorderingPossibleFromSecondRow() {
// given
openTestURL();
toggleColumnReordering();
selectMenuPath("Component", "Header", "Append row");
assertColumnHeaderOrder(0, 1, 2, 3);

// when
dragAndDropColumnHeader(1, 0, 2, CellSide.RIGHT);

// then
assertColumnHeaderOrder(1, 2, 0, 3);
}

private void toggleColumnReordering() {
selectMenuPath(COLUMN_REORDERING_PATH);
}

private void toggleColumnReorderListener() {
selectMenuPath(COLUMN_REORDER_LISTENER_PATH);
}

private void moveColumnManuallyLeftByOne(int index) {
selectMenuPath(new String[] { "Component", "Columns",
"Column " + index, "Move left" });
}

private void assertColumnReorderEvent(boolean userOriginated) {
final String logRow = getLogRow(0);
assertTrue(logRow.contains("Columns reordered, userOriginated: "
+ userOriginated));
}

private void assertNoColumnReorderEvent() {
final String logRow = getLogRow(0);
assertFalse(logRow.contains("Columns reordered"));
}

}

+ 280
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnVisibilityTest.java View File

@@ -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.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 org.junit.Before;
import org.junit.Test;

import com.vaadin.testbench.parallel.TestCategory;
import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;

@TestCategory("grid")
public class GridColumnVisibilityTest extends GridBasicFeaturesTest {

private static final String[] TOGGLE_LISTENER = new String[] { "Component",
"State", "ColumnVisibilityChangeListener" };
private static final String[] TOGGLE_HIDE_COLUMN_0 = new String[] {
"Component", "Columns", "Column 0", "Hidden" };

private static final String COLUMN_0_BECAME_HIDDEN_MSG = "Visibility "
+ "changed: propertyId: Column 0, isHidden: true";
private static final String COLUMN_0_BECAME_UNHIDDEN_MSG = "Visibility "
+ "changed: propertyId: Column 0, isHidden: false";
private static final String USER_ORIGINATED_TRUE = "userOriginated: true";
private static final String USER_ORIGINATED_FALSE = "userOriginated: false";

@Before
public void setUp() {
openTestURL();
}

@Test
public void columnIsNotShownWhenHidden() {
assertEquals("column 0", getGridElement().getHeaderCell(0, 0).getText()
.toLowerCase());

selectMenuPath(TOGGLE_HIDE_COLUMN_0);
assertEquals("column 1", getGridElement().getHeaderCell(0, 0).getText()
.toLowerCase());
}

@Test
public void columnIsShownWhenUnhidden() {
selectMenuPath(TOGGLE_HIDE_COLUMN_0);
selectMenuPath(TOGGLE_HIDE_COLUMN_0);
assertEquals("column 0", getGridElement().getHeaderCell(0, 0).getText()
.toLowerCase());
}

@Test
public void registeringListener() {
assertFalse(logContainsText(COLUMN_0_BECAME_HIDDEN_MSG));
selectMenuPath(TOGGLE_LISTENER);
assertFalse(logContainsText(COLUMN_0_BECAME_HIDDEN_MSG));

selectMenuPath(TOGGLE_HIDE_COLUMN_0);
assertTrue(logContainsText(COLUMN_0_BECAME_HIDDEN_MSG));
assertTrue(logContainsText(USER_ORIGINATED_FALSE));

selectMenuPath(TOGGLE_HIDE_COLUMN_0);
assertTrue(logContainsText(COLUMN_0_BECAME_UNHIDDEN_MSG));
assertTrue(logContainsText(USER_ORIGINATED_FALSE));
}

@Test
public void deregisteringListener() {
selectMenuPath(TOGGLE_LISTENER);
selectMenuPath(TOGGLE_HIDE_COLUMN_0);

selectMenuPath(TOGGLE_LISTENER);
selectMenuPath(TOGGLE_HIDE_COLUMN_0);
assertFalse(logContainsText(COLUMN_0_BECAME_UNHIDDEN_MSG));
}

@Test
public void testColumnHiding_userOriginated_correctParams() {
selectMenuPath(TOGGLE_LISTENER);
toggleColumnHidable(0);
assertColumnHeaderOrder(0, 1, 2, 3);

getSidebarOpenButton().click();
getColumnHidingToggle(0).click();
getSidebarOpenButton().click();

assertColumnHeaderOrder(1, 2, 3);
assertTrue(logContainsText(COLUMN_0_BECAME_HIDDEN_MSG));
assertTrue(logContainsText(USER_ORIGINATED_TRUE));

getSidebarOpenButton().click();
getColumnHidingToggle(0).click();
getSidebarOpenButton().click();

assertColumnHeaderOrder(0, 1, 2, 3);
assertTrue(logContainsText(COLUMN_0_BECAME_UNHIDDEN_MSG));
assertTrue(logContainsText(USER_ORIGINATED_TRUE));

getSidebarOpenButton().click();
getColumnHidingToggle(0).click();
getSidebarOpenButton().click();

assertColumnHeaderOrder(1, 2, 3);
assertTrue(logContainsText(COLUMN_0_BECAME_HIDDEN_MSG));
assertTrue(logContainsText(USER_ORIGINATED_TRUE));
}

@Test
public void testColumnHiding_whenHidableColumnRemoved_toggleRemoved() {
toggleColumnHidable(0);
toggleColumnHidable(1);
getSidebarOpenButton().click();
assertNotNull(getColumnHidingToggle(0));

addRemoveColumn(0);

assertNull(getColumnHidingToggle(0));
}

@Test
public void testColumnHiding_whenHidableColumnAdded_toggleWithCorrectCaptionAdded() {
selectMenuPath("Component", "Size", "Width", "100%");
toggleColumnHidable(0);
toggleColumnHidable(1);
toggleColumnHidingToggleCaptionChange(0);
getSidebarOpenButton().click();
assertEquals("Column 0 caption 0", getColumnHidingToggle(0).getText());
getSidebarOpenButton().click();

addRemoveColumn(0);
addRemoveColumn(4);
addRemoveColumn(5);
addRemoveColumn(6);
addRemoveColumn(7);
addRemoveColumn(8);
addRemoveColumn(9);
addRemoveColumn(10);
assertColumnHeaderOrder(1, 2, 3, 11);

getSidebarOpenButton().click();
assertNull(getColumnHidingToggle(0));
getSidebarOpenButton().click();

addRemoveColumn(0);
assertColumnHeaderOrder(1, 2, 3, 11, 0);

getSidebarOpenButton().click();
assertEquals("Column 0 caption 0", getColumnHidingToggle(0).getText());
}

@Test
public void testColumnHidingToggleCaption_settingToggleCaption_updatesToggle() {
toggleColumnHidable(1);
getSidebarOpenButton().click();
assertEquals("column 1", getGridElement().getHeaderCell(0, 1).getText()
.toLowerCase());
assertEquals("Column 1", getColumnHidingToggle(1).getText());

toggleColumnHidingToggleCaptionChange(1);
assertEquals("column 1", getGridElement().getHeaderCell(0, 1).getText()
.toLowerCase());
assertEquals("Column 1 caption 0", getColumnHidingToggle(1).getText());

toggleColumnHidingToggleCaptionChange(1);
assertEquals("Column 1 caption 1", getColumnHidingToggle(1).getText());
}

@Test
public void testColumnHidingToggleCaption_settingWidgetToHeader_toggleCaptionStays() {
toggleColumnHidable(1);
getSidebarOpenButton().click();
assertEquals("column 1", getGridElement().getHeaderCell(0, 1).getText()
.toLowerCase());
assertEquals("Column 1", getColumnHidingToggle(1).getText());

selectMenuPath("Component", "Columns", "Column 1", "Header Type",
"Widget Header");

assertEquals("Column 1", getColumnHidingToggle(1).getText());
}

private void toggleColumnHidingToggleCaptionChange(int index) {
selectMenuPath("Component", "Columns", "Column " + index,
"Change hiding toggle caption");
}

@Test
public void testFrozenColumnHiding_hiddenColumnMadeFrozen_frozenWhenMadeVisible() {
selectMenuPath("Component", "Size", "Width", "100%");
toggleColumnHidable(0);
toggleColumnHidable(1);
getSidebarOpenButton().click();
getColumnHidingToggle(0).click();
getColumnHidingToggle(1).click();

assertColumnHeaderOrder(2, 3, 4, 5);

setFrozenColumns(2);
verifyColumnNotFrozen(0);
verifyColumnNotFrozen(1);

getColumnHidingToggle(0).click();
assertColumnHeaderOrder(0, 2, 3, 4, 5);
verifyColumnFrozen(0);
verifyColumnNotFrozen(1);

getColumnHidingToggle(1).click();
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5);
verifyColumnFrozen(0);
verifyColumnFrozen(1);
verifyColumnNotFrozen(2);
}

@Test
public void testFrozenColumnHiding_hiddenFrozenColumnUnfrozen_notFrozenWhenMadeVisible() {
selectMenuPath("Component", "Size", "Width", "100%");
toggleColumnHidable(0);
toggleColumnHidable(1);
setFrozenColumns(2);
verifyColumnFrozen(0);
verifyColumnFrozen(1);
verifyColumnNotFrozen(2);
verifyColumnNotFrozen(3);

getSidebarOpenButton().click();
getColumnHidingToggle(0).click();
getColumnHidingToggle(1).click();
assertColumnHeaderOrder(2, 3, 4, 5);
verifyColumnNotFrozen(0);
verifyColumnNotFrozen(1);

setFrozenColumns(0);
verifyColumnNotFrozen(0);
verifyColumnNotFrozen(1);

getColumnHidingToggle(0).click();
assertColumnHeaderOrder(0, 2, 3, 4, 5);
verifyColumnNotFrozen(0);
verifyColumnNotFrozen(1);

getColumnHidingToggle(1).click();
assertColumnHeaderOrder(0, 1, 2, 3, 4, 5);
verifyColumnNotFrozen(0);
verifyColumnNotFrozen(1);
verifyColumnNotFrozen(2);
}

private void verifyColumnFrozen(int index) {
assertTrue(getGridElement().getHeaderCell(0, index).isFrozen());
}

private void verifyColumnNotFrozen(int index) {
assertFalse(getGridElement().getHeaderCell(0, index).isFrozen());
}

private void toggleColumnHidable(int index) {
selectMenuPath("Component", "Columns", "Column " + index, "Hidable");
}

private void addRemoveColumn(int index) {
selectMenuPath("Component", "Columns", "Column " + index,
"Add / Remove");
}
}

+ 306
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java View File

@@ -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.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.assertTrue;
import static org.junit.Assert.fail;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;

import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.NotificationElement;
import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;

public class GridDetailsServerTest extends GridBasicFeaturesTest {
/**
* The reason to why last item details wasn't selected is that since it will
* exist only after the viewport has been scrolled into view, we wouldn't be
* able to scroll that particular details row into view, making tests
* awkward with two scroll commands back to back.
*/
private static final int ALMOST_LAST_INDEX = 995;
private static final String[] OPEN_ALMOST_LAST_ITEM_DETAILS = new String[] {
"Component", "Details", "Open " + ALMOST_LAST_INDEX };
private static final String[] OPEN_FIRST_ITEM_DETAILS = new String[] {
"Component", "Details", "Open firstItemId" };
private static final String[] TOGGLE_FIRST_ITEM_DETAILS = new String[] {
"Component", "Details", "Toggle firstItemId" };
private static final String[] DETAILS_GENERATOR_NULL = new String[] {
"Component", "Details", "Generators", "NULL" };
private static final String[] DETAILS_GENERATOR_WATCHING = new String[] {
"Component", "Details", "Generators", "\"Watching\"" };
private static final String[] DETAILS_GENERATOR_HIERARCHICAL = new String[] {
"Component", "Details", "Generators", "Hierarchical" };
private static final String[] CHANGE_HIERARCHY = new String[] {
"Component", "Details", "Generators", "- Change Component" };

@Before
public void setUp() {
openTestURL();
}

@Test
public void openVisibleDetails() {
try {
getGridElement().getDetails(0);
fail("Expected NoSuchElementException");
} catch (NoSuchElementException ignore) {
// expected
}
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
assertNotNull("details should've opened", getGridElement()
.getDetails(0));
}

@Test(expected = NoSuchElementException.class)
public void closeVisibleDetails() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);

getGridElement().getDetails(0);
}

@Test
public void openVisiblePopulatedDetails() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
assertNotNull("details should've populated", getGridElement()
.getDetails(0).findElement(By.className("v-widget")));
}

@Test(expected = NoSuchElementException.class)
public void closeVisiblePopulatedDetails() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
getGridElement().getDetails(0);
}

@Test
public void openDetailsOutsideOfActiveRange() throws InterruptedException {
getGridElement().scroll(10000);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
getGridElement().scroll(0);
Thread.sleep(50);
assertNotNull("details should've been opened", getGridElement()
.getDetails(0));
}

@Test(expected = NoSuchElementException.class)
public void closeDetailsOutsideOfActiveRange() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
getGridElement().scroll(10000);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
getGridElement().scroll(0);
getGridElement().getDetails(0);
}

@Test
public void componentIsVisibleClientSide() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);

TestBenchElement details = getGridElement().getDetails(0);
assertNotNull("No widget detected inside details",
details.findElement(By.className("v-widget")));
}

@Test
public void openingDetailsTwice() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // close
selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open

TestBenchElement details = getGridElement().getDetails(0);
assertNotNull("No widget detected inside details",
details.findElement(By.className("v-widget")));
}

@Test(expected = NoSuchElementException.class)
public void scrollingDoesNotCreateAFloodOfDetailsRows() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);

// scroll somewhere to hit uncached rows
getGridElement().scrollToRow(101);

// this should throw
getGridElement().getDetails(100);
}

@Test
public void openingDetailsOutOfView() {
getGridElement().scrollToRow(500);

selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);

getGridElement().scrollToRow(0);

// if this fails, it'll fail before the assertNotNull
assertNotNull("unexpected null details row", getGridElement()
.getDetails(0));
}

@Test
public void togglingAVisibleDetailsRowWithOneRoundtrip() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open

assertTrue("Unexpected generator content",
getGridElement().getDetails(0).getText().endsWith("(0)"));
selectMenuPath(TOGGLE_FIRST_ITEM_DETAILS);
assertTrue("New component was not displayed in the client",
getGridElement().getDetails(0).getText().endsWith("(1)"));
}

@Test
public void almostLastItemIdIsRendered() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_ALMOST_LAST_ITEM_DETAILS);
scrollGridVerticallyTo(100000);

TestBenchElement details = getGridElement().getDetails(
ALMOST_LAST_INDEX);
assertNotNull(details);
assertTrue("Unexpected details content",
details.getText().endsWith(ALMOST_LAST_INDEX + " (0)"));
}

@Test
public void hierarchyChangesWorkInDetails() {
selectMenuPath(DETAILS_GENERATOR_HIERARCHICAL);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
assertEquals("One", getGridElement().getDetails(0).getText());
selectMenuPath(CHANGE_HIERARCHY);
assertEquals("Two", getGridElement().getDetails(0).getText());
}

@Ignore("This use case is not currently supported by Grid. If the detail "
+ "is out of view, the component is detached from the UI and a "
+ "new instance is generated when scrolled back. Support will "
+ "maybe be incorporated at a later time")
@Test
public void hierarchyChangesWorkInDetailsWhileOutOfView() {
selectMenuPath(DETAILS_GENERATOR_HIERARCHICAL);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
scrollGridVerticallyTo(10000);
selectMenuPath(CHANGE_HIERARCHY);
scrollGridVerticallyTo(0);
assertEquals("Two", getGridElement().getDetails(0).getText());
}

@Test
public void swappingDetailsGenerators_noDetailsShown() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(DETAILS_GENERATOR_NULL);
assertFalse("Got some errors", $(NotificationElement.class).exists());
}

@Test
public void swappingDetailsGenerators_shownDetails() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
assertTrue("Details should be empty at the start", getGridElement()
.getDetails(0).getText().isEmpty());

selectMenuPath(DETAILS_GENERATOR_WATCHING);
assertFalse("Details should not be empty after swapping generator",
getGridElement().getDetails(0).getText().isEmpty());
}

@Test
public void swappingDetailsGenerators_whileDetailsScrolledOut_showNever() {
scrollGridVerticallyTo(1000);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(DETAILS_GENERATOR_WATCHING);
assertFalse("Got some errors", $(NotificationElement.class).exists());
}

@Test
public void swappingDetailsGenerators_whileDetailsScrolledOut_showAfter() {
scrollGridVerticallyTo(1000);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(DETAILS_GENERATOR_WATCHING);
scrollGridVerticallyTo(0);

assertFalse("Got some errors", $(NotificationElement.class).exists());
assertNotNull("Could not find a details", getGridElement()
.getDetails(0));
}

@Test
public void swappingDetailsGenerators_whileDetailsScrolledOut_showBefore() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(DETAILS_GENERATOR_WATCHING);
scrollGridVerticallyTo(1000);

assertFalse("Got some errors", $(NotificationElement.class).exists());
assertNotNull("Could not find a details", getGridElement()
.getDetails(0));
}

@Test
public void swappingDetailsGenerators_whileDetailsScrolledOut_showBeforeAndAfter() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(DETAILS_GENERATOR_WATCHING);
scrollGridVerticallyTo(1000);
scrollGridVerticallyTo(0);

assertFalse("Got some errors", $(NotificationElement.class).exists());
assertNotNull("Could not find a details", getGridElement()
.getDetails(0));
}

@Test
public void nullDetailComponentToggling() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(DETAILS_GENERATOR_NULL);

try {
assertTrue("Details should be empty with null component",
getGridElement().getDetails(0).getText().isEmpty());
} catch (NoSuchElementException e) {
fail("Expected to find a details row with empty content");
}

selectMenuPath(DETAILS_GENERATOR_WATCHING);
assertFalse("Details should be not empty with details component",
getGridElement().getDetails(0).getText().isEmpty());
}

@Test
public void noAssertErrorsOnEmptyDetailsAndScrollDown() {
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
scrollGridVerticallyTo(500);
assertFalse(logContainsText("AssertionError"));
}

@Test
public void noAssertErrorsOnPopulatedDetailsAndScrollDown() {
selectMenuPath(DETAILS_GENERATOR_WATCHING);
selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
scrollGridVerticallyTo(500);
assertFalse(logContainsText("AssertionError"));
}
}

+ 84
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSidebarThemeTest.java View File

@@ -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.components.grid.basicfeatures.server;

import java.io.IOException;
import java.util.List;

import org.junit.Test;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.remote.DesiredCapabilities;

import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
import com.vaadin.tests.components.grid.basicfeatures.GridSidebarFeatures;

public class GridSidebarThemeTest extends GridBasicFeaturesTest {

@Test
public void testValo() throws Exception {
runTestSequence("valo");
}

@Test
public void testValoDark() throws Exception {
runTestSequence("tests-valo-dark");
}

@Override
protected Class<?> getUIClass() {
return GridSidebarFeatures.class;
}

private void runTestSequence(String theme) throws IOException {
openTestURL("theme=" + theme);

compareScreen(theme + "|SidebarClosed");
getSidebarOpenButton().click();

compareScreen(theme + "|SidebarOpen");

new Actions(getDriver()).moveToElement(getColumnHidingToggle(2), 5, 5)
.perform();

compareScreen(theme + "|OnMouseOverNotHiddenToggle");

getColumnHidingToggle(2).click();
getColumnHidingToggle(3).click();
getColumnHidingToggle(6).click();

new Actions(getDriver()).moveToElement(getSidebarOpenButton())
.perform();
;

compareScreen(theme + "|TogglesTriggered");

new Actions(getDriver()).moveToElement(getColumnHidingToggle(2))
.perform();
;

compareScreen(theme + "|OnMouseOverHiddenToggle");

getSidebarOpenButton().click();

compareScreen(theme + "|SidebarClosed2");
}

@Override
public List<DesiredCapabilities> getBrowsersToTest() {
// phantom JS looks wrong from the beginning, so not tested
return getBrowsersExcludingPhantomJS();
}
}

+ 0
- 7
uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java View File

@@ -19,7 +19,6 @@ import org.junit.Assert;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;

@@ -83,10 +82,4 @@ public class LoadingIndicatorTest extends GridBasicFeaturesTest {
});
}

private boolean isLoadingIndicatorVisible() {
WebElement loadingIndicator = findElement(By
.className("v-loading-indicator"));

return loadingIndicator.isDisplayed();
}
}

+ 20
- 0
uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java View File

@@ -938,4 +938,24 @@ public abstract class AbstractTB3Test extends ParallelTest {
protected void click(CheckBoxElement checkbox) {
checkbox.findElement(By.xpath("input")).click();
}

protected boolean isLoadingIndicatorVisible() {
WebElement loadingIndicator = findElement(By
.className("v-loading-indicator"));

return loadingIndicator.isDisplayed();
}

protected void waitUntilLoadingIndicatorNotVisible() {
waitUntil(new ExpectedCondition<Boolean>() {

@Override
public Boolean apply(WebDriver input) {
WebElement loadingIndicator = input.findElement(By
.className("v-loading-indicator"));

return !loadingIndicator.isDisplayed();
}
});
}
}

+ 5
- 1
uitest/src/com/vaadin/tests/util/PersonContainer.java View File

@@ -32,10 +32,14 @@ public class PersonContainer extends BeanItemContainer<Person> implements
}

public static PersonContainer createWithTestData() {
return createWithTestData(100);
}

public static PersonContainer createWithTestData(int size) {
PersonContainer c = null;
Random r = new Random(0);
c = new PersonContainer();
for (int i = 0; i < 100; i++) {
for (int i = 0; i < size; i++) {
Person p = new Person();
p.setFirstName(TestDataGenerator.getFirstName(r));
p.setLastName(TestDataGenerator.getLastName(r));

+ 114
- 0
uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java View File

@@ -6,13 +6,18 @@ 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.DOM;
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.widget.escalator.RowContainer.BodyRowContainer;
import com.vaadin.client.widget.escalator.Spacer;
import com.vaadin.client.widget.escalator.SpacerUpdater;
import com.vaadin.client.widgets.Escalator;
import com.vaadin.shared.ui.grid.ScrollDestination;

public class EscalatorBasicClientFeaturesWidget extends
PureGWTTestApplication<Escalator> {
@@ -303,6 +308,7 @@ public class EscalatorBasicClientFeaturesWidget extends
createColumnsAndRowsMenu();
createFrozenMenu();
createColspanMenu();
createSpacerMenu();
}

private void createFrozenMenu() {
@@ -567,6 +573,26 @@ public class EscalatorBasicClientFeaturesWidget extends
escalator.setScrollTop(40);
}
}, menupath);

String[] scrollToRowMenuPath = new String[menupath.length + 1];
System.arraycopy(menupath, 0, scrollToRowMenuPath, 0, menupath.length);
scrollToRowMenuPath[scrollToRowMenuPath.length - 1] = "Scroll to...";
for (int i = 0; i < 100; i += 25) {
final int rowIndex = i;
addMenuCommand("Row " + i, new ScheduledCommand() {
@Override
public void execute() {
escalator.scrollToRow(rowIndex, ScrollDestination.ANY, 0);
}
}, scrollToRowMenuPath);
}

addMenuCommand("Set 20px default height", new ScheduledCommand() {
@Override
public void execute() {
escalator.getBody().setDefaultRowHeight(20);
}
}, menupath);
}

private void createRowsMenu(final RowContainer container, String[] menupath) {
@@ -612,6 +638,94 @@ public class EscalatorBasicClientFeaturesWidget extends
}, menupath);
}

private void createSpacerMenu() {
String[] menupath = { "Features", "Spacers" };

addMenuCommand("Swap Spacer Updater", new ScheduledCommand() {
private final SpacerUpdater CUSTOM = new SpacerUpdater() {
@Override
public void destroy(Spacer spacer) {
spacer.getElement().setInnerText("");
}

@Override
public void init(Spacer spacer) {
spacer.getElement().setInnerText(
"Spacer for row " + spacer.getRow());
}
};

@Override
public void execute() {
BodyRowContainer body = escalator.getBody();

if (SpacerUpdater.NULL.equals(body.getSpacerUpdater())) {
body.setSpacerUpdater(CUSTOM);
} else {
body.setSpacerUpdater(SpacerUpdater.NULL);
}
}
}, menupath);

addMenuCommand("Focusable Updater", new ScheduledCommand() {
@Override
public void execute() {
escalator.getBody().setSpacerUpdater(new SpacerUpdater() {
@Override
public void init(Spacer spacer) {
spacer.getElement().appendChild(DOM.createInputText());
}

@Override
public void destroy(Spacer spacer) {
spacer.getElement().removeAllChildren();
}
});
}
}, menupath);

createSpacersMenuForRow(-1, menupath);
createSpacersMenuForRow(1, menupath);
createSpacersMenuForRow(50, menupath);
createSpacersMenuForRow(99, menupath);
}

private void createSpacersMenuForRow(final int rowIndex, String[] menupath) {
menupath = new String[] { menupath[0], menupath[1], "Row " + rowIndex };
addMenuCommand("Set 100px", new ScheduledCommand() {
@Override
public void execute() {
escalator.getBody().setSpacer(rowIndex, 100);
}
}, menupath);
addMenuCommand("Set 50px", new ScheduledCommand() {
@Override
public void execute() {
escalator.getBody().setSpacer(rowIndex, 50);
}
}, menupath);
addMenuCommand("Remove", new ScheduledCommand() {
@Override
public void execute() {
escalator.getBody().setSpacer(rowIndex, -1);
}
}, menupath);
addMenuCommand("Scroll here (ANY, 0)", new ScheduledCommand() {
@Override
public void execute() {
escalator.scrollToSpacer(rowIndex, ScrollDestination.ANY, 0);
}
}, menupath);
addMenuCommand("Scroll here row+spacer below (ANY, 0)",
new ScheduledCommand() {
@Override
public void execute() {
escalator.scrollToRowAndSpacer(rowIndex,
ScrollDestination.ANY, 0);
}
}, menupath);
}

private void insertRows(final RowContainer container, int offset, int number) {
if (container == escalator.getBody()) {
data.insertRows(offset, number);

+ 32
- 3
uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java View File

@@ -24,6 +24,8 @@ 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.widget.escalator.RowContainer.BodyRowContainer;
import com.vaadin.client.widget.escalator.SpacerUpdater;
import com.vaadin.client.widgets.Escalator;
import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget;

@@ -97,6 +99,33 @@ public class EscalatorProxy extends Escalator {
}
}

private class BodyRowContainerProxy extends RowContainerProxy implements
BodyRowContainer {
private BodyRowContainer rowContainer;

public BodyRowContainerProxy(BodyRowContainer rowContainer) {
super(rowContainer);
this.rowContainer = rowContainer;
}

@Override
public void setSpacer(int rowIndex, double height)
throws IllegalArgumentException {
rowContainer.setSpacer(rowIndex, height);
}

@Override
public void setSpacerUpdater(SpacerUpdater spacerUpdater)
throws IllegalArgumentException {
rowContainer.setSpacerUpdater(spacerUpdater);
}

@Override
public SpacerUpdater getSpacerUpdater() {
return rowContainer.getSpacerUpdater();
}
}

private class RowContainerProxy implements RowContainer {
private final RowContainer rowContainer;

@@ -176,7 +205,7 @@ public class EscalatorProxy extends Escalator {
}

private RowContainer headerProxy = null;
private RowContainer bodyProxy = null;
private BodyRowContainer bodyProxy = null;
private RowContainer footerProxy = null;
private ColumnConfiguration columnProxy = null;
private LogWidget logWidget;
@@ -198,9 +227,9 @@ public class EscalatorProxy extends Escalator {
}

@Override
public RowContainer getBody() {
public BodyRowContainer getBody() {
if (bodyProxy == null) {
bodyProxy = new RowContainerProxy(super.getBody());
bodyProxy = new BodyRowContainerProxy(super.getBody());
}
return bodyProxy;
}

+ 343
- 1
uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java View File

@@ -33,9 +33,13 @@ 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.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.MenuItemSeparator;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.renderers.DateRenderer;
@@ -46,6 +50,7 @@ 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.DetailsGenerator;
import com.vaadin.client.widget.grid.EditorHandler;
import com.vaadin.client.widget.grid.RendererCellReference;
import com.vaadin.client.widget.grid.RowReference;
@@ -55,6 +60,10 @@ 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.ColumnReorderEvent;
import com.vaadin.client.widget.grid.events.ColumnReorderHandler;
import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent;
import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler;
import com.vaadin.client.widget.grid.events.FooterKeyDownHandler;
import com.vaadin.client.widget.grid.events.FooterKeyPressHandler;
import com.vaadin.client.widget.grid.events.FooterKeyUpHandler;
@@ -73,6 +82,7 @@ import com.vaadin.client.widgets.Grid.Column;
import com.vaadin.client.widgets.Grid.FooterRow;
import com.vaadin.client.widgets.Grid.HeaderRow;
import com.vaadin.client.widgets.Grid.SelectionMode;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget.Data;

/**
@@ -400,6 +410,8 @@ public class GridBasicClientFeaturesWidget extends
createEditorMenu();
createInternalsMenu();
createDataSourceMenu();
createDetailsMenu();
createSidebarMenu();

grid.getElement().getStyle().setZIndex(0);

@@ -444,6 +456,71 @@ public class GridBasicClientFeaturesWidget extends
});
}
}, listenersPath);
addMenuCommand("Add ColumnReorder listener", new ScheduledCommand() {
private HandlerRegistration columnReorderHandler = null;

@Override
public void execute() {
if (columnReorderHandler != null) {
return;
}
final Label columnOrderLabel = new Label();
columnOrderLabel.getElement().setId("columnreorder");
addLineEnd(columnOrderLabel, 300);
columnReorderHandler = grid
.addColumnReorderHandler(new ColumnReorderHandler<List<Data>>() {

private int eventIndex = 0;

@Override
public void onColumnReorder(
ColumnReorderEvent<List<Data>> event) {
columnOrderLabel.getElement().setAttribute(
"columns", "" + (++eventIndex));
}
});
}
}, listenersPath);
addMenuCommand("Add Column Visibility Change listener",
new ScheduledCommand() {
private HandlerRegistration columnVisibilityHandler = null;

@Override
public void execute() {
if (columnVisibilityHandler != null) {
return;
}
final Label columnOrderLabel = new Label();
columnOrderLabel.getElement().setId("columnvisibility");
addLineEnd(columnOrderLabel, 250);
ColumnVisibilityChangeHandler handler = new ColumnVisibilityChangeHandler<List<Data>>() {

private int eventIndex = 0;

@Override
public void onVisibilityChange(
ColumnVisibilityChangeEvent<List<Data>> event) {
columnOrderLabel.getElement().setAttribute(
"counter", "" + (++eventIndex));
columnOrderLabel.getElement().setAttribute(
"useroriginated",
(Boolean.toString(event
.isUserOriginated())));
columnOrderLabel.getElement().setAttribute(
"ishidden",
(Boolean.toString(event.isHidden())));
columnOrderLabel.getElement().setAttribute(
"columnindex",
""
+ grid.getColumns().indexOf(
event.getColumn()));
}
};

columnVisibilityHandler = grid
.addColumnVisibilityChangeHandler(handler);
}
}, listenersPath);
}

private void createStateMenu() {
@@ -658,6 +735,79 @@ public class GridBasicClientFeaturesWidget extends
grid.setColumnOrder(columns.toArray(new Column[columns.size()]));
}
}, "Component", "State");
addMenuCommand("Column Reordering", new ScheduledCommand() {

@Override
public void execute() {
grid.setColumnReorderingAllowed(!grid
.isColumnReorderingAllowed());
}
}, "Component", "State");
addMenuCommand("250px", new ScheduledCommand() {

@Override
public void execute() {
grid.setWidth("250px");
}
}, "Component", "State", "Width");
addMenuCommand("500px", new ScheduledCommand() {

@Override
public void execute() {
grid.setWidth("500px");
}
}, "Component", "State", "Width");
addMenuCommand("750px", new ScheduledCommand() {

@Override
public void execute() {
grid.setWidth("750px");
}
}, "Component", "State", "Width");
addMenuCommand("1000px", new ScheduledCommand() {

@Override
public void execute() {
grid.setWidth("1000px");
}
}, "Component", "State", "Width");

createScrollToRowMenu();
}

private void createScrollToRowMenu() {
String[] menupath = new String[] { "Component", "State",
"Scroll to...", null };

for (int i = 0; i < ROWS; i += 100) {
menupath[3] = "Row " + i + "...";
for (final ScrollDestination scrollDestination : ScrollDestination
.values()) {
final int row = i;
addMenuCommand("Destination " + scrollDestination,
new ScheduledCommand() {
@Override
public void execute() {
grid.scrollToRow(row, scrollDestination);
}
}, menupath);
}
}

int i = ROWS - 1;
menupath[3] = "Row " + i + "...";
for (final ScrollDestination scrollDestination : ScrollDestination
.values()) {
final int row = i;
addMenuCommand("Destination " + scrollDestination,
new ScheduledCommand() {
@Override
public void execute() {
grid.scrollToRow(row, scrollDestination);
}
}, menupath);
}

}

private void createColumnsMenu() {
@@ -671,7 +821,18 @@ public class GridBasicClientFeaturesWidget extends
column.setSortable(!column.isSortable());
}
}, "Component", "Columns", "Column " + i);

addMenuCommand("Hidden", new ScheduledCommand() {
@Override
public void execute() {
column.setHidden(!column.isHidden());
}
}, "Component", "Columns", "Column " + i);
addMenuCommand("Hidable", new ScheduledCommand() {
@Override
public void execute() {
column.setHidable(!column.isHidable());
}
}, "Component", "Columns", "Column " + i);
addMenuCommand("auto", new ScheduledCommand() {
@Override
public void execute() {
@@ -768,6 +929,25 @@ public class GridBasicClientFeaturesWidget extends
});
}
}, "Component", "Columns", "Column " + i);
addMenuCommand("Move column left", new ScheduledCommand() {

@SuppressWarnings("unchecked")
@Override
public void execute() {
List<Column<?, List<Data>>> cols = grid.getColumns();
ArrayList<Column> reordered = new ArrayList<Column>(cols);
final int index = cols.indexOf(column);
if (index == 0) {
Column<?, List<Data>> col = reordered.remove(0);
reordered.add(col);
} else {
Column<?, List<Data>> col = reordered.remove(index);
reordered.add(index - 1, col);
}
grid.setColumnOrder(reordered.toArray(new Column[reordered
.size()]));
}
}, "Component", "Columns", "Column " + i);
}
}

@@ -1223,4 +1403,166 @@ public class GridBasicClientFeaturesWidget extends
String coords = "(" + object + ", " + column + ")";
label.setText(coords + " " + output);
}

private void createDetailsMenu() {
String[] menupath = new String[] { "Component", "Row details" };
addMenuCommand("Set generator", new ScheduledCommand() {
@Override
public void execute() {
grid.setDetailsGenerator(new DetailsGenerator() {
@Override
public Widget getDetails(int rowIndex) {
FlowPanel panel = new FlowPanel();

final Label label = new Label("Row: " + rowIndex + ".");
Button button = new Button("Button",
new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
label.setText("clicked");
}
});

panel.add(label);
panel.add(button);
return panel;
}
});
}
}, menupath);

addMenuCommand("Set faulty generator", new ScheduledCommand() {
@Override
public void execute() {
grid.setDetailsGenerator(new DetailsGenerator() {
@Override
public Widget getDetails(int rowIndex) {
throw new RuntimeException("This is by design.");
}
});
}
}, menupath);

addMenuCommand("Set empty generator", new ScheduledCommand() {
@Override
public void execute() {
grid.setDetailsGenerator(new DetailsGenerator() {
/*
* While this is functionally equivalent to the NULL
* generator, it's good to be explicit, since the behavior
* isn't strictly tied between them. NULL generator might be
* changed to render something different by default, and an
* empty generator might behave differently also in the
* future.
*/

@Override
public Widget getDetails(int rowIndex) {
return null;
}
});
}
}, menupath);

String[] togglemenupath = new String[] { menupath[0], menupath[1],
"Toggle details for..." };
for (int i : new int[] { 0, 1, 100, 200, 300, 400, 500, 600, 700, 800,
900, 999 }) {
final int rowIndex = i;
addMenuCommand("Row " + rowIndex, new ScheduledCommand() {
boolean visible = false;

@Override
public void execute() {
visible = !visible;
grid.setDetailsVisible(rowIndex, visible);
}
}, togglemenupath);
}

}

private static Logger getLogger() {
return Logger.getLogger(GridBasicClientFeaturesWidget.class.getName());
}

private void createSidebarMenu() {
String[] menupath = new String[] { "Component", "Sidebar" };

final List<MenuItem> customMenuItems = new ArrayList<MenuItem>();
final List<MenuItemSeparator> separators = new ArrayList<MenuItemSeparator>();

addMenuCommand("Add item to end", new ScheduledCommand() {
@Override
public void execute() {
MenuItem item = createSidebarMenuItem(customMenuItems.size());
customMenuItems.add(item);
grid.getSidebarMenu().addItem(item);
}
}, menupath);

addMenuCommand("Add item before index 1", new ScheduledCommand() {
@Override
public void execute() {
MenuItem item = createSidebarMenuItem(customMenuItems.size());
customMenuItems.add(item);
grid.getSidebarMenu().insertItem(item, 1);
}
}, menupath);

addMenuCommand("Remove last added item", new ScheduledCommand() {
@Override
public void execute() {
grid.getSidebarMenu().removeItem(
customMenuItems.remove(customMenuItems.size() - 1));
}
}, menupath);

addMenuCommand("Add separator to end", new ScheduledCommand() {
@Override
public void execute() {
MenuItemSeparator separator = new MenuItemSeparator();
separators.add(separator);
grid.getSidebarMenu().addSeparator(separator);
}
}, menupath);

addMenuCommand("Add separator before index 1", new ScheduledCommand() {
@Override
public void execute() {
MenuItemSeparator separator = new MenuItemSeparator();
separators.add(separator);
grid.getSidebarMenu().insertSeparator(separator, 1);
}
}, menupath);

addMenuCommand("Remove last added separator", new ScheduledCommand() {
@Override
public void execute() {
grid.getSidebarMenu().removeSeparator(
separators.remove(separators.size() - 1));
}
}, menupath);

addMenuCommand("Toggle sidebar visibility", new ScheduledCommand() {
@Override
public void execute() {
grid.setSidebarOpen(!grid.isSidebarOpen());
}
}, menupath);
}

private MenuItem createSidebarMenuItem(final int index) {
final MenuItem menuItem = new MenuItem("Custom menu item " + index,
new ScheduledCommand() {
@Override
public void execute() {
if (index % 2 == 0) {
grid.setSidebarOpen(false);
}
getLogger().info("Menu item " + index + " selected");
}
});
return menuItem;
}
}

+ 1
- 0
widgets/build.xml View File

@@ -55,6 +55,7 @@
<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/ui/dd/DragAndDropHandler.java" />
<include name="com/vaadin/client/VSchedulerImpl.java" />

<include name="com/vaadin/shared/ui/grid/*.java" />

Loading…
Cancel
Save