diff options
61 files changed, 11575 insertions, 999 deletions
diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss index 606dc6a7dd..bae95b299c 100644 --- a/WebContent/VAADIN/themes/base/escalator/escalator.scss +++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss @@ -133,4 +133,23 @@ z-index: 1; } + .#{$primaryStyleName}-spacer { + position: absolute; + display: block; + + background-color: $background-color; + + > td { + width: 100%; + height: 100%; + } + + .v-ie8 &, .v-ie9 & { + // The inline style of margin-top from the <tbody> to offset the + // header's dimension is, for some strange reason, inherited into each + // contained <tr>. We need to cancel it: + + margin-top: 0; + } + } } diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss index 8a1497dda8..838c0de502 100644 --- a/WebContent/VAADIN/themes/base/grid/grid.scss +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -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,102 @@ $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); + 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; + 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 { + background: #fff; + border-top: $v-grid-border; + padding: 4px 0; + + .gwt-MenuBar { + .gwt-MenuItem .column-hiding-toggle { + text-shadow: none; + } + } + } + } // Common cell styles @@ -331,6 +435,52 @@ $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 { + position: relative; + top: $v-grid-border-size; + 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 { diff --git a/WebContent/VAADIN/themes/reindeer/grid/grid.scss b/WebContent/VAADIN/themes/reindeer/grid/grid.scss index 28c5977fda..7ae0f402aa 100644 --- a/WebContent/VAADIN/themes/reindeer/grid/grid.scss +++ b/WebContent/VAADIN/themes/reindeer/grid/grid.scss @@ -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 { diff --git a/WebContent/VAADIN/themes/runo/grid/grid.scss b/WebContent/VAADIN/themes/runo/grid/grid.scss index 06a04ec626..71ec6e06e0 100644 --- a/WebContent/VAADIN/themes/runo/grid/grid.scss +++ b/WebContent/VAADIN/themes/runo/grid/grid.scss @@ -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, diff --git a/WebContent/VAADIN/themes/valo/components/_grid.scss b/WebContent/VAADIN/themes/valo/components/_grid.scss index 4cac9c5e43..c07d330410 100644 --- a/WebContent/VAADIN/themes/valo/components/_grid.scss +++ b/WebContent/VAADIN/themes/valo/components/_grid.scss @@ -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 diff --git a/WebContent/release-notes.html b/WebContent/release-notes.html index 48116a027c..97730cc292 100644 --- a/WebContent/release-notes.html +++ b/WebContent/release-notes.html @@ -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>) diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index 5e9dfc6101..d31baaa665 100644 --- a/client/src/com/vaadin/client/connectors/GridConnector.java +++ b/client/src/com/vaadin/client/connectors/GridConnector.java @@ -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; + } } diff --git a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java index f8d6ebcb62..627ee74eca 100644 --- a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java @@ -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); } } diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index 1de271c646..88977d85ec 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -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; @@ -570,6 +584,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)) { diff --git a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java new file mode 100644 index 0000000000..61fdd2850e --- /dev/null +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -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; + } + } +} diff --git a/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java b/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java index 6109c5e51d..54507a7650 100644 --- a/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java +++ b/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java @@ -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 diff --git a/client/src/com/vaadin/client/widget/escalator/Row.java b/client/src/com/vaadin/client/widget/escalator/Row.java index bcb3e163e4..fa89853120 100644 --- a/client/src/com/vaadin/client/widget/escalator/Row.java +++ b/client/src/com/vaadin/client/widget/escalator/Row.java @@ -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}. diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java index 397336450e..abab25046c 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -22,17 +22,102 @@ 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. * */ diff --git a/client/src/com/vaadin/client/widget/escalator/Spacer.java b/client/src/com/vaadin/client/widget/escalator/Spacer.java new file mode 100644 index 0000000000..789a64a21e --- /dev/null +++ b/client/src/com/vaadin/client/widget/escalator/Spacer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.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(); +} diff --git a/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java new file mode 100644 index 0000000000..49adefd536 --- /dev/null +++ b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.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); +} diff --git a/client/src/com/vaadin/client/widget/grid/AutoScroller.java b/client/src/com/vaadin/client/widget/grid/AutoScroller.java new file mode 100644 index 0000000000..f2e44196ec --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -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 < + * {@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(); + } +} diff --git a/client/src/com/vaadin/client/widget/grid/CellReference.java b/client/src/com/vaadin/client/widget/grid/CellReference.java index a2e841de43..e783cb92ae 100644 --- a/client/src/com/vaadin/client/widget/grid/CellReference.java +++ b/client/src/com/vaadin/client/widget/grid/CellReference.java @@ -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 */ @@ -90,6 +102,17 @@ public class CellReference<T> { } /** + * 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. * * @return the column object @@ -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); } /** diff --git a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java new file mode 100644 index 0000000000..b9427091a7 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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); +} diff --git a/client/src/com/vaadin/client/widget/grid/EventCellReference.java b/client/src/com/vaadin/client/widget/grid/EventCellReference.java index cf13798e11..7ca1d5de75 100644 --- a/client/src/com/vaadin/client/widget/grid/EventCellReference.java +++ b/client/src/com/vaadin/client/widget/grid/EventCellReference.java @@ -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(); } diff --git a/client/src/com/vaadin/client/widget/grid/RendererCellReference.java b/client/src/com/vaadin/client/widget/grid/RendererCellReference.java index 533eafded6..994db50aa0 100644 --- a/client/src/com/vaadin/client/widget/grid/RendererCellReference.java +++ b/client/src/com/vaadin/client/widget/grid/RendererCellReference.java @@ -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); } /** diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java new file mode 100644 index 0000000000..1712871089 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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); + } + +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java new file mode 100644 index 0000000000..29c476058e --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.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); +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java new file mode 100644 index 0000000000..63b788bcf2 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java @@ -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); + } + +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java new file mode 100644 index 0000000000..542fe4e3c1 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.EventHandler; + +/** + * 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); +} diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 83c176d6fd..eae5789b8a 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -16,6 +16,7 @@ package com.vaadin.client.widgets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -24,6 +25,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -52,12 +54,14 @@ import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.DeferredWorker; import com.vaadin.client.Profiler; import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.SubPartAware; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; @@ -69,11 +73,14 @@ import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition; import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition; import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.widget.escalator.RowContainer; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.ScrollbarBundle; import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.client.widget.escalator.Spacer; +import com.vaadin.client.widget.escalator.SpacerUpdater; import com.vaadin.client.widget.grid.events.ScrollEvent; import com.vaadin.client.widget.grid.events.ScrollHandler; import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle; @@ -95,7 +102,7 @@ import com.vaadin.shared.util.SharedUtil; |-- AbstractStaticRowContainer | |-- HeaderRowContainer | `-- FooterContainer - `---- BodyRowContainer + `---- BodyRowContainerImpl AbstractRowContainer is intended to contain all common logic between RowContainers. It manages the bookkeeping of row @@ -108,7 +115,7 @@ import com.vaadin.shared.util.SharedUtil; are pretty thin special cases of a StaticRowContainer (mostly relating to positioning of the root element). - BodyRowContainer could also be split into an additional + BodyRowContainerImpl could also be split into an additional "AbstractScrollingRowContainer", but I felt that no more inner classes were needed. So it contains both logic required for making things scroll about, and equivalent @@ -120,8 +127,8 @@ import com.vaadin.shared.util.SharedUtil; Each RowContainer can be thought to have three levels of indices for any given displayed row (but the distinction - matters primarily for the BodyRowContainer, because of the - way it scrolls through data): + matters primarily for the BodyRowContainerImpl, because of + the way it scrolls through data): - Logical index - Physical (or DOM) index @@ -139,9 +146,9 @@ import com.vaadin.shared.util.SharedUtil; (because of 0-based indices). In Header and FooterRowContainers, you are safe to assume that the logical index is the same as the physical index. But because the - BodyRowContainer never displays large data sources entirely - in the DOM, a physical index usually has no apparent direct - relationship with its logical index. + BodyRowContainerImpl never displays large data sources + entirely in the DOM, a physical index usually has no + apparent direct relationship with its logical index. VISUAL INDEX is the index relating to the order that you see a row in, in the browser, as it is rendered. The @@ -149,20 +156,20 @@ import com.vaadin.shared.util.SharedUtil; index is similar to the physical index in the sense that Header and FooterRowContainers can assume a 1:1 relationship between visual index and logical index. And - again, BodyRowContainer has no such relationship. The + again, BodyRowContainerImpl has no such relationship. The body's visual index has additionally no apparent relationship with its physical index. Because the <tr> tags are reused in the body and visually repositioned with CSS as the user scrolls, the relationship between physical index and visual index is quickly broken. You can get an element's visual index via the field - BodyRowContainer.visualRowOrder. + BodyRowContainerImpl.visualRowOrder. Currently, the physical and visual indices are kept in sync _most of the time_ by a deferred rearrangement of rows. They become desynced when scrolling. This is to help screen readers to read the contents from the DOM in a natural - order. See BodyRowContainer.DeferredDomSorter for more + order. See BodyRowContainerImpl.DeferredDomSorter for more about that. */ @@ -265,7 +272,8 @@ abstract class JsniWorkaround { * @since 7.4 * @author Vaadin Ltd */ -public class Escalator extends Widget implements RequiresResize, DeferredWorker { +public class Escalator extends Widget implements RequiresResize, + DeferredWorker, SubPartAware { // todo comments legend /* @@ -274,18 +282,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * re-measure) */ /* - * [[rowheight]]: This code will require alterations that are relevant for - * being able to support variable row heights. NOTE: these bits can most - * often also be identified by searching for code reading the ROW_HEIGHT_PX - * constant. - */ - /* * [[mpixscroll]]: This code will require alterations that are relevant for * supporting the scrolling through more pixels than some browsers normally * would support. (i.e. when we support more than "a million" pixels in the * escalator DOM). NOTE: these bits can most often also be identified by * searching for code that call scrollElem.getScrollTop();. */ + /* + * [[spacer]]: Code that is important to make spacers work. + */ /** * A utility class that contains utility methods that are usually called @@ -813,7 +818,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * that the sizes of the scroll handles appear correct in the browser */ public void recalculateScrollbarsForVirtualViewport() { - double scrollContentHeight = body.calculateTotalRowHeight(); + double scrollContentHeight = body.calculateTotalRowHeight() + + body.spacerContainer.getSpacerHeightsSum(); double scrollContentWidth = columnConfiguration.calculateRowWidth(); double tableWrapperHeight = heightOfEscalator; @@ -821,8 +827,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + WidgetUtil.PIXEL_EPSILON - - header.heightOfSection - - footer.heightOfSection; + - header.getHeightOfSection() + - footer.getHeightOfSection(); boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth + WidgetUtil.PIXEL_EPSILON; @@ -831,8 +837,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (!verticalScrollNeeded && horizontalScrollNeeded) { verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + WidgetUtil.PIXEL_EPSILON - - header.heightOfSection - - footer.heightOfSection + - header.getHeightOfSection() + - footer.getHeightOfSection() - horizontalScrollbar.getScrollbarThickness(); } else { horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth @@ -854,8 +860,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX); tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX); + double footerHeight = footer.getHeightOfSection(); + double headerHeight = header.getHeightOfSection(); double vScrollbarHeight = Math.max(0, tableWrapperHeight - - footer.heightOfSection - header.heightOfSection); + - footerHeight - headerHeight); verticalScrollbar.setOffsetSize(vScrollbarHeight); verticalScrollbar.setScrollSize(scrollContentHeight); @@ -954,6 +962,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker lastScrollTop = scrollTop; body.updateEscalatorRowsOnScroll(); + body.spacerContainer.updateSpacerDecosVisibility(); /* * TODO [[optimize]]: Might avoid a reflow by first calculating new * scrolltop and scrolleft, then doing the escalator magic based on @@ -1143,17 +1152,16 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker public void scrollToRow(final int rowIndex, final ScrollDestination destination, final double padding) { - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - final double targetStartPx = body.getDefaultRowHeight() * rowIndex; + + final double targetStartPx = (body.getDefaultRowHeight() * rowIndex) + + body.spacerContainer + .getSpacerHeightsSumUntilIndex(rowIndex); final double targetEndPx = targetStartPx + body.getDefaultRowHeight(); final double viewportStartPx = getScrollTop(); final double viewportEndPx = viewportStartPx - + body.calculateHeight(); + + body.getHeightOfSection(); final double scrollTop = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); @@ -1178,28 +1186,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ protected final TableSectionElement root; - /** The height of the combined rows in the DOM. Never negative. */ - protected double heightOfSection = 0; - /** * The primary style name of the escalator. Most commonly provided by * Escalator as "v-escalator". */ private String primaryStyleName = null; - /** - * A map containing cached values of an element's current top position. - * <p> - * Don't use this field directly, because it will not take proper care - * of all the bookkeeping required. - * - * @deprecated Use {@link #setRowPosition(Element, int, int)}, - * {@link #getRowTop(Element)} and - * {@link #removeRowPosition(Element)} instead. - */ - @Deprecated - private final Map<TableRowElement, Double> rowTopPositionMap = new HashMap<TableRowElement, Double>(); - private boolean defaultRowHeightShouldBeAutodetected = true; private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; @@ -1711,6 +1703,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); + if (!rowCanBeFrozen(tr)) { + continue; + } TableCellElement cell = tr.getCells().getItem(column); if (frozen) { @@ -1732,12 +1727,29 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); - TableCellElement cell = tr.getCells().getItem(column); - position.set(cell, scrollLeft, 0); + if (rowCanBeFrozen(tr)) { + TableCellElement cell = tr.getCells().getItem(column); + position.set(cell, scrollLeft, 0); + } } } /** + * Checks whether a row is an element, or contains such elements, that + * can be frozen. + * <p> + * In practice, this applies for all header and footer rows. For body + * rows, it applies for all rows except spacer rows. + * + * @param tr + * the row element to check for if it is or has elements that + * can be frozen + * @return <code>true</code> iff this the given element, or any of its + * descendants, can be frozen + */ + abstract protected boolean rowCanBeFrozen(TableRowElement tr); + + /** * Iterates through all the cells in a column and returns the width of * the widest element in this RowContainer. * @@ -1912,20 +1924,29 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ } - @SuppressWarnings("boxing") protected void setRowPosition(final TableRowElement tr, final int x, final double y) { - position.set(tr, x, y); - rowTopPositionMap.put(tr, y); + positions.set(tr, x, y); } - @SuppressWarnings("boxing") + /** + * Returns <em>the assigned</em> top position for the given element. + * <p> + * <em>Note:</em> This method does not calculate what a row's top + * position should be. It just returns an assigned value, correct or + * not. + * + * @param tr + * the table row element to measure + * @return the current top position for {@code tr} + * @see BodyRowContainerImpl#getRowTop(int) + */ protected double getRowTop(final TableRowElement tr) { - return rowTopPositionMap.get(tr); + return positions.getTop(tr); } protected void removeRowPosition(TableRowElement tr) { - rowTopPositionMap.remove(tr); + positions.remove(tr); } public void autodetectRowHeightLater() { @@ -2021,7 +2042,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker TableCellElement cellOriginal = rowElement.getCells().getItem( colIndex); - if (cellIsPartOfSpan(cellOriginal)) { + if (cellOriginal == null || cellIsPartOfSpan(cellOriginal)) { continue; } @@ -2060,10 +2081,24 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker refreshCells(rowRange, colRange); } } + + /** + * The height of this table section. + * <p> + * Note that {@link Escalator#getBody() the body} will calculate its + * height, while the others will return a precomputed value. + * + * @return the height of this table section + */ + protected abstract double getHeightOfSection(); } private abstract class AbstractStaticRowContainer extends AbstractRowContainer { + + /** The height of the combined rows in the DOM. Never negative. */ + private double heightOfSection = 0; + public AbstractStaticRowContainer(final TableSectionElement headElement) { super(headElement); } @@ -2157,9 +2192,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * indices are calculated from the scrollbar position. */ verticalScrollbar.setOffsetSize(heightOfEscalator - - header.heightOfSection - footer.heightOfSection); + - header.getHeightOfSection() + - footer.getHeightOfSection()); body.verifyEscalatorCount(); + body.spacerContainer.updateSpacerDecosVisibility(); } Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight"); @@ -2188,18 +2225,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - /* - * TODO [[rowheight]]: even if no rows are evaluated in the current - * viewport, the heights of some unrendered rows might change in a - * refresh. This would cause the scrollbar to be adjusted (in - * scrollHeight and/or scrollTop). Do we want to take this into - * account? - */ if (hasColumnAndRowData()) { - /* - * TODO [[rowheight]]: nudge rows down with - * refreshRowPositions() as needed - */ for (int row = logicalRowRange.getStart(); row < logicalRowRange .getEnd(); row++) { final TableRowElement tr = getTrByVisualIndex(row); @@ -2214,6 +2240,17 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker protected void paintInsertRows(int visualIndex, int numberOfRows) { paintInsertStaticRows(visualIndex, numberOfRows); } + + @Override + protected boolean rowCanBeFrozen(TableRowElement tr) { + assert root.isOrHasChild(tr) : "Row does not belong to this table section"; + return true; + } + + @Override + protected double getHeightOfSection() { + return Math.max(0, heightOfSection); + } } private class HeaderRowContainer extends AbstractStaticRowContainer { @@ -2223,7 +2260,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker @Override protected void sectionHeightCalculated() { + double heightOfSection = getHeightOfSection(); bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX); + spacerDecoContainer.getStyle().setMarginTop(heightOfSection, + Unit.PX); verticalScrollbar.getElement().getStyle() .setTop(heightOfSection, Unit.PX); headerDeco.getStyle().setHeight(heightOfSection, Unit.PX); @@ -2259,8 +2299,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker @Override protected void sectionHeightCalculated() { + double headerHeight = header.getHeightOfSection(); + double footerHeight = footer.getHeightOfSection(); int vscrollHeight = (int) Math.floor(heightOfEscalator - - header.heightOfSection - footer.heightOfSection); + - headerHeight - footerHeight); final boolean horizontalScrollbarNeeded = columnConfiguration .calculateRowWidth() > widthOfEscalator; @@ -2268,13 +2310,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker vscrollHeight -= horizontalScrollbar.getScrollbarThickness(); } - footerDeco.getStyle().setHeight(footer.heightOfSection, Unit.PX); + footerDeco.getStyle().setHeight(footer.getHeightOfSection(), + Unit.PX); verticalScrollbar.setOffsetSize(vscrollHeight); } } - private class BodyRowContainer extends AbstractRowContainer { + private class BodyRowContainerImpl extends AbstractRowContainer implements + BodyRowContainer { /* * TODO [[optimize]]: check whether a native JsArray might be faster * than LinkedList @@ -2316,7 +2360,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker this.topRowLogicalIndex = topRowLogicalIndex; } - private int getTopRowLogicalIndex() { + public int getTopRowLogicalIndex() { return topRowLogicalIndex; } @@ -2385,7 +2429,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private DeferredDomSorter domSorter = new DeferredDomSorter(); - public BodyRowContainer(final TableSectionElement bodyElement) { + private final SpacerContainer spacerContainer = new SpacerContainer(); + + public BodyRowContainerImpl(final TableSectionElement bodyElement) { super(bodyElement); } @@ -2393,6 +2439,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker public void setStylePrimaryName(String primaryStyleName) { super.setStylePrimaryName(primaryStyleName); UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); + spacerContainer.setStylePrimaryName(primaryStyleName); } public void updateEscalatorRowsOnScroll() { @@ -2402,10 +2449,23 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker boolean rowsWereMoved = false; - final double topRowPos = getRowTop(visualRowOrder.getFirst()); + final double topElementPosition; + final double nextRowBottomOffset; + SpacerContainer.SpacerImpl topSpacer = spacerContainer + .getSpacer(getTopRowLogicalIndex() - 1); + + if (topSpacer != null) { + topElementPosition = topSpacer.getTop(); + nextRowBottomOffset = topSpacer.getHeight() + + getDefaultRowHeight(); + } else { + topElementPosition = getRowTop(visualRowOrder.getFirst()); + nextRowBottomOffset = getDefaultRowHeight(); + } + // TODO [[mpixscroll]] final double scrollTop = tBodyScrollTop; - final double viewportOffset = topRowPos - scrollTop; + final double viewportOffset = topElementPosition - scrollTop; /* * TODO [[optimize]] this if-else can most probably be refactored @@ -2415,22 +2475,17 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (viewportOffset > 0) { // there's empty room on top - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - int originalRowsToMove = (int) Math.ceil(viewportOffset + double rowPx = getRowHeightsSumBetweenPx(scrollTop, + topElementPosition); + int originalRowsToMove = (int) Math.ceil(rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, - root.getChildCount()); + visualRowOrder.size()); - final int end = root.getChildCount(); + final int end = visualRowOrder.size(); final int start = end - rowsToMove; - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + final int logicalRowIndex = getLogicalRowIndex(scrollTop); + moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); @@ -2439,24 +2494,21 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker rowsWereMoved = true; } - else if (viewportOffset + getDefaultRowHeight() <= 0) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - + else if (viewportOffset + nextRowBottomOffset <= 0) { /* * the viewport has been scrolled more than the topmost visual * row. */ - int originalRowsToMove = (int) Math.abs(viewportOffset - / getDefaultRowHeight()); + double rowPx = getRowHeightsSumBetweenPx(topElementPosition, + scrollTop); + + int originalRowsToMove = (int) (rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, - root.getChildCount()); + visualRowOrder.size()); int logicalRowIndex; - if (rowsToMove < root.getChildCount()) { + if (rowsToMove < visualRowOrder.size()) { /* * We scroll so little that we can just keep adding the rows * below the current escalator @@ -2465,15 +2517,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .getLast()) + 1; } else { /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - /* * Since we're moving all escalator rows, we need to * calculate the first logical row index from the scroll * position. */ - logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + logicalRowIndex = getLogicalRowIndex(scrollTop); } /* @@ -2482,13 +2530,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * moveAndUpdateEscalatorRows works, this will work out even if * we move all the rows, and try to place them "at the end". */ - final int targetVisualIndex = root.getChildCount(); + final int targetVisualIndex = visualRowOrder.size(); // make sure that we don't move rows over the data boundary boolean aRowWasLeftBehind = false; if (logicalRowIndex + rowsToMove > getRowCount()) { /* - * TODO [[rowheight]]: with constant row heights, there's + * TODO [[spacer]]: with constant row heights, there's * always exactly one row that will be moved beyond the data * source, when viewport is scrolled to the end. This, * however, isn't guaranteed anymore once row heights start @@ -2498,6 +2546,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker aRowWasLeftBehind = true; } + /* + * Make sure we don't scroll beyond the row content. This can + * happen if we have spacers for the last rows. + */ + rowsToMove = Math.max(0, + Math.min(rowsToMove, getRowCount() - logicalRowIndex)); + moveAndUpdateEscalatorRows(Range.between(0, rowsToMove), targetVisualIndex, logicalRowIndex); @@ -2544,12 +2599,30 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + private double getRowHeightsSumBetweenPx(double y1, double y2) { + assert y1 < y2 : "y1 must be smaller than y2"; + + double viewportPx = y2 - y1; + double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1, + SpacerInclusionStrategy.PARTIAL, y2, + SpacerInclusionStrategy.PARTIAL); + + return viewportPx - spacerPx; + } + + private int getLogicalRowIndex(final double px) { + double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px); + return (int) (rowPx / getDefaultRowHeight()); + } + @Override protected void paintInsertRows(final int index, final int numberOfRows) { if (numberOfRows == 0) { return; } + spacerContainer.shiftSpacersByRows(index, numberOfRows); + /* * TODO: this method should probably only add physical rows, and not * populate them - let everything be populated as appropriate by the @@ -2567,15 +2640,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ scroller.recalculateScrollbarsForVirtualViewport(); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final boolean addedRowsAboveCurrentViewport = index * getDefaultRowHeight() < getScrollTop(); final boolean addedRowsBelowCurrentViewport = index * getDefaultRowHeight() > getScrollTop() - + calculateHeight(); + + getHeightOfSection(); if (addedRowsAboveCurrentViewport) { /* @@ -2584,12 +2653,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * without re-evaluating any rows. */ - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double yDelta = numberOfRows * getDefaultRowHeight(); - adjustScrollPosIgnoreEvents(yDelta); + moveViewportAndContent(yDelta); updateTopRowLogicalIndex(numberOfRows); } @@ -2612,32 +2677,32 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * the remaining rows aswell. */ final int rowsStillNeeded = numberOfRows - addedRows.size(); - final Range unupdatedVisual = convertToVisual(Range.withLength( - unupdatedLogicalStart, rowsStillNeeded)); - final int end = root.getChildCount(); - final int start = end - unupdatedVisual.length(); - final int visualTargetIndex = unupdatedLogicalStart - - visualOffset; - moveAndUpdateEscalatorRows(Range.between(start, end), - visualTargetIndex, unupdatedLogicalStart); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - // move the surrounding rows to their correct places. - double rowTop = (unupdatedLogicalStart + (end - start)) - * getDefaultRowHeight(); - final ListIterator<TableRowElement> i = visualRowOrder - .listIterator(visualTargetIndex + (end - start)); - while (i.hasNext()) { - final TableRowElement tr = i.next(); - setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - rowTop += getDefaultRowHeight(); + if (rowsStillNeeded > 0) { + final Range unupdatedVisual = convertToVisual(Range + .withLength(unupdatedLogicalStart, rowsStillNeeded)); + final int end = getEscalatorRowCount(); + final int start = end - unupdatedVisual.length(); + final int visualTargetIndex = unupdatedLogicalStart + - visualOffset; + moveAndUpdateEscalatorRows(Range.between(start, end), + visualTargetIndex, unupdatedLogicalStart); + + // move the surrounding rows to their correct places. + double rowTop = (unupdatedLogicalStart + (end - start)) + * getDefaultRowHeight(); + final ListIterator<TableRowElement> i = visualRowOrder + .listIterator(visualTargetIndex + (end - start)); + + int logicalRowIndexCursor = unupdatedLogicalStart; + while (i.hasNext()) { + rowTop += spacerContainer + .getSpacerHeight(logicalRowIndexCursor++); + + final TableRowElement tr = i.next(); + setRowPosition(tr, 0, rowTop); + rowTop += getDefaultRowHeight(); + } } fireRowVisibilityChangeEvent(); @@ -2655,12 +2720,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * the visual index where the rows will be placed to * @param logicalTargetIndex * the logical index to be assigned to the first moved row - * @throws IllegalArgumentException - * if any of <code>visualSourceRange.getStart()</code>, - * <code>visualTargetIndex</code> or - * <code>logicalTargetIndex</code> is a negative number; or - * if <code>visualTargetInfo</code> is greater than the - * number of escalator rows. */ private void moveAndUpdateEscalatorRows(final Range visualSourceRange, final int visualTargetIndex, final int logicalTargetIndex) @@ -2670,28 +2729,28 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - if (visualSourceRange.getStart() < 0) { - throw new IllegalArgumentException( - "Logical source start must be 0 or greater (was " - + visualSourceRange.getStart() + ")"); - } else if (logicalTargetIndex < 0) { - throw new IllegalArgumentException( - "Logical target must be 0 or greater"); - } else if (visualTargetIndex < 0) { - throw new IllegalArgumentException( - "Visual target must be 0 or greater"); - } else if (visualTargetIndex > root.getChildCount()) { - throw new IllegalArgumentException( - "Visual target must not be greater than the number of escalator rows"); - } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) { - Range logicalTargetRange = Range.withLength(logicalTargetIndex, - visualSourceRange.length()); - Range availableRange = Range.withLength(0, getRowCount()); - throw new IllegalArgumentException("Logical target leads " - + "to rows outside of the data range (" - + logicalTargetRange + " goes beyond " + availableRange - + ")"); - } + assert visualSourceRange.getStart() >= 0 : "Visual source start " + + "must be 0 or greater (was " + + visualSourceRange.getStart() + ")"; + + assert logicalTargetIndex >= 0 : "Logical target must be 0 or " + + "greater (was " + logicalTargetIndex + ")"; + + assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was " + + visualTargetIndex + ")"; + + assert visualTargetIndex <= getEscalatorRowCount() : "Visual target " + + "must not be greater than the number of escalator rows (was " + + visualTargetIndex + + ", escalator rows " + + getEscalatorRowCount() + ")"; + + assert logicalTargetIndex + visualSourceRange.length() <= getRowCount() : "Logical " + + "target leads to rows outside of the data range (" + + Range.withLength(logicalTargetIndex, + visualSourceRange.length()) + + " goes beyond " + + Range.withLength(0, getRowCount()) + ")"; /* * Since we move a range into another range, the indices might move @@ -2746,55 +2805,74 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } { // Reposition the rows that were moved - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - double newRowTop = logicalTargetIndex * getDefaultRowHeight(); + double newRowTop = getRowTop(logicalTargetIndex); final ListIterator<TableRowElement> iter = visualRowOrder .listIterator(adjustedVisualTargetIndex); for (int i = 0; i < visualSourceRange.length(); i++) { final TableRowElement tr = iter.next(); setRowPosition(tr, 0, newRowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ + newRowTop += getDefaultRowHeight(); + newRowTop += spacerContainer + .getSpacerHeight(logicalTargetIndex + i); } } } /** - * Adjust the scroll position without having the scroll handler have any - * side-effects. + * Adjust the scroll position and move the contained rows. * <p> - * <em>Note:</em> {@link Scroller#onScroll()} <em>will</em> be - * triggered, but it will not do anything, with the help of - * {@link Escalator#internalScrollEventCalls}. + * The difference between using this method and simply scrolling is that + * this method "takes the rows and spacers with it" and renders them + * appropriately. The viewport may be scrolled any arbitrary amount, and + * the contents are moved appropriately, but always snapped into a + * plausible place. + * <p> + * <dl> + * <dt>Example 1</dt> + * <dd>An Escalator with default row height 20px. Adjusting the scroll + * position with 7.5px will move the viewport 7.5px down, but leave the + * row where it is.</dd> + * <dt>Example 2</dt> + * <dd>An Escalator with default row height 20px. Adjusting the scroll + * position with 27.5px will move the viewport 27.5px down, and place + * the row at 20px.</dd> + * </dl> * * @param yDelta - * the delta of pixels to scrolls. A positive value moves the - * viewport downwards, while a negative value moves the - * viewport upwards + * the delta of pixels by which to move the viewport and + * content. A positive value moves everything downwards, + * while a negative value moves everything upwards */ - public void adjustScrollPosIgnoreEvents(final double yDelta) { + public void moveViewportAndContent(final double yDelta) { + if (yDelta == 0) { return; } - verticalScrollbar.setScrollPosByDelta(yDelta); + double newTop = tBodyScrollTop + yDelta; + verticalScrollbar.setScrollPos(newTop); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - final double rowTopPos = yDelta - (yDelta % getDefaultRowHeight()); - for (final TableRowElement tr : visualRowOrder) { - setRowPosition(tr, 0, getRowTop(tr) + rowTopPos); + final double defaultRowHeight = getDefaultRowHeight(); + double rowPxDelta = yDelta - (yDelta % defaultRowHeight); + int rowIndexDelta = (int) (yDelta / defaultRowHeight); + if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) { + + Collection<SpacerContainer.SpacerImpl> spacers = spacerContainer + .getSpacersAfterPx(tBodyScrollTop, + SpacerInclusionStrategy.PARTIAL); + for (SpacerContainer.SpacerImpl spacer : spacers) { + spacer.setPositionDiff(0, rowPxDelta); + spacer.setRowIndex(spacer.getRow() + rowIndexDelta); + } + + for (TableRowElement tr : visualRowOrder) { + setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta); + } } - setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta); + + setBodyScrollPosition(tBodyScrollLeft, newTop); } /** @@ -2816,7 +2894,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final int index, final int numberOfRows) { final int escalatorRowsStillFit = getMaxEscalatorRowCapacity() - - root.getChildCount(); + - getEscalatorRowCount(); final int escalatorRowsNeeded = Math.min(numberOfRows, escalatorRowsStillFit); @@ -2826,43 +2904,31 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker index, escalatorRowsNeeded); visualRowOrder.addAll(index, addedRows); - /* - * We need to figure out the top positions for the rows we just - * added. - */ - for (int i = 0; i < addedRows.size(); i++) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - setRowPosition(addedRows.get(i), 0, (index + i) - * getDefaultRowHeight()); - } + double y = index * getDefaultRowHeight() + + spacerContainer.getSpacerHeightsSumUntilIndex(index); + for (int i = index; i < visualRowOrder.size(); i++) { - /* Move the other rows away from above the added escalator rows */ - for (int i = index + addedRows.size(); i < visualRowOrder - .size(); i++) { - final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - setRowPosition(tr, 0, i * getDefaultRowHeight()); + final TableRowElement tr; + if (i - index < addedRows.size()) { + tr = addedRows.get(i - index); + } else { + tr = visualRowOrder.get(i); + } + + setRowPosition(tr, 0, y); + y += getDefaultRowHeight(); + y += spacerContainer.getSpacerHeight(i); } return addedRows; } else { - return new ArrayList<TableRowElement>(); + return Collections.emptyList(); } } private int getMaxEscalatorRowCapacity() { - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final int maxEscalatorRowCapacity = (int) Math - .ceil(calculateHeight() / getDefaultRowHeight()) + 1; + .ceil(getHeightOfSection() / getDefaultRowHeight()) + 1; /* * maxEscalatorRowCapacity can become negative if the headers and @@ -2882,6 +2948,18 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final Range removedRowsRange = Range .withLength(index, numberOfRows); + /* + * Removing spacers as the very first step will correct the + * scrollbars and row offsets right away. + * + * TODO: actually, it kinda sounds like a Grid feature that a spacer + * would be associated with a particular row. Maybe it would be + * better to have a spacer separate from rows, and simply collapse + * them if they happen to end up on top of each other. This would + * probably make supporting the -1 row pretty easy, too. + */ + spacerContainer.paintRemoveSpacers(removedRowsRange); + final Range[] partitions = removedRowsRange .partitionWith(viewportRange); final Range removedAbove = partitions[0]; @@ -2907,17 +2985,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * absolute 0) * * The logic is optimized in such a way that the - * adjustScrollPosIgnoreEvents is called only once, to avoid extra + * moveViewportAndContent is called only once, to avoid extra * reflows, and thus the code might seem a bit obscure. */ final boolean firstVisualRowIsRemoved = !removedVisualInside .isEmpty() && removedVisualInside.getStart() == 0; if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double yDelta = removedAbove.length() * getDefaultRowHeight(); final double firstLogicalRowHeight = getDefaultRowHeight(); @@ -2931,7 +3005,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * to do is to adjust the scroll position to account for the * removed rows */ - adjustScrollPosIgnoreEvents(-yDelta); + moveViewportAndContent(-yDelta); } else if (removalScrollsToShowFirstLogicalRow) { /* * It seems like we've removed all rows from above, and also @@ -2940,14 +3014,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * current negative scrolltop, presto!), so that it isn't * aligned funnily */ - adjustScrollPosIgnoreEvents(-verticalScrollbar - .getScrollPos()); + moveViewportAndContent(-verticalScrollbar.getScrollPos()); } } // ranges evaluated, let's do things. if (!removedVisualInside.isEmpty()) { - int escalatorRowCount = bodyElem.getChildCount(); + int escalatorRowCount = body.getEscalatorRowCount(); /* * remember: the rows have already been subtracted from the row @@ -2978,13 +3051,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * visualIndex == logicalIndex applies now. */ final int dirtyRowsStart = removedLogicalInside.getStart(); + double y = getRowTop(dirtyRowsStart); for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ - setRowPosition(tr, 0, i * getDefaultRowHeight()); + setRowPosition(tr, 0, y); + y += getDefaultRowHeight(); + y += spacerContainer.getSpacerHeight(i); } /* @@ -3023,14 +3095,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * double-refreshing. */ - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double contentBottom = getRowCount() * getDefaultRowHeight(); final double viewportBottom = tBodyScrollTop - + calculateHeight(); + + getHeightOfSection(); if (viewportBottom <= contentBottom) { /* * We're in the middle of the row container, everything @@ -3081,7 +3149,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ /* - * FIXME [[rowheight]]: above if-clause is coded to only + * FIXME [[spacer]]: above if-clause is coded to only * work with default row heights - will not work with * variable row heights */ @@ -3144,13 +3212,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); setRowPosition(tr, 0, (int) newTop); - - /* - * FIXME [[rowheight]]: coded to work only with - * default row heights - will not work with variable - * row heights - */ newTop += getDefaultRowHeight(); + newTop += spacerContainer.getSpacerHeight(i + + removedLogicalInside.getStart()); } /* @@ -3166,7 +3230,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 5 5 */ final double newScrollTop = contentBottom - - calculateHeight(); + - getHeightOfSection(); setScrollTop(newScrollTop); /* * Manually call the scroll handler, so we get immediate @@ -3198,10 +3262,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 5 */ - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ final int rowsScrolled = (int) (Math .ceil((viewportBottom - contentBottom) / getDefaultRowHeight())); @@ -3253,21 +3313,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final ListIterator<TableRowElement> iterator = visualRowOrder .listIterator(removedVisualInside.getStart()); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - double rowTop = (removedLogicalInside.getStart() + logicalOffset) - * getDefaultRowHeight(); + double rowTop = getRowTop(removedLogicalInside.getStart() + + logicalOffset); for (int i = removedVisualInside.getStart(); i < escalatorRowCount - removedVisualInside.length(); i++) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); + rowTop += spacerContainer.getSpacerHeight(i + + removedLogicalInside.getStart()); } } @@ -3287,22 +3341,18 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker logicalTargetIndex); // move the surrounding rows to their correct places. + int firstUpdatedIndex = removedVisualInside.getEnd(); final ListIterator<TableRowElement> iterator = visualRowOrder - .listIterator(removedVisualInside.getEnd()); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - double rowTop = removedLogicalInside.getStart() - * getDefaultRowHeight(); + .listIterator(firstUpdatedIndex); + + double rowTop = getRowTop(removedLogicalInside.getStart()); + int i = 0; while (iterator.hasNext()) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); + rowTop += spacerContainer.getSpacerHeight(firstUpdatedIndex + + i++); } } @@ -3340,6 +3390,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * current viewport. The first visual row has the index 0. */ private Range convertToVisual(final Range logicalRange) { + if (logicalRange.isEmpty()) { return logicalRange; } else if (visualRowOrder.isEmpty()) { @@ -3348,8 +3399,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /* - * TODO [[rowheight]]: these assumptions will be totally broken with - * variable row heights. + * TODO [[spacer]]: these assumptions will be totally broken with + * spacers. */ final int maxEscalatorRows = getMaxEscalatorRowCapacity(); final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder @@ -3366,15 +3417,14 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return "td"; } - /** - * Calculates the height of the {@code <tbody>} as it should be rendered - * in the DOM. - */ - private double calculateHeight() { + @Override + protected double getHeightOfSection() { final int tableHeight = tableWrapper.getOffsetHeight(); - final double footerHeight = footer.heightOfSection; - final double headerHeight = header.heightOfSection; - return tableHeight - footerHeight - headerHeight; + final double footerHeight = footer.getHeightOfSection(); + final double headerHeight = header.getHeightOfSection(); + + double heightOfSection = tableHeight - footerHeight - headerHeight; + return Math.max(0, heightOfSection); } @Override @@ -3428,6 +3478,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker tBodyScrollLeft = scrollLeft; tBodyScrollTop = scrollTop; position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); + position.set(spacerDecoContainer, 0, -tBodyScrollTop); } /** @@ -3570,10 +3621,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (!visualRowOrder.isEmpty()) { final double firstRowTop = getRowTop(visualRowOrder .getFirst()); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double firstRowMinTop = tBodyScrollTop - getDefaultRowHeight(); if (firstRowTop < firstRowMinTop) { @@ -3598,20 +3645,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - /* - * As an intermediate step between hard-coded row heights to crazily - * varying row heights, Escalator will support the modification of - * the default row height (which is applied to all rows). - * - * This allows us to do some assumptions and simplifications for - * now. This code is intended to be quite short-lived, but gives - * insight into what needs to be done when row heights change in the - * body, in a general sense. - * - * TODO [[rowheight]] remove this comment once row heights may - * genuinely vary. - */ - Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights"); /* step 1: resize and reposition rows */ @@ -3645,10 +3678,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker /* step 3: make sure we have the correct amount of escalator rows. */ verifyEscalatorCount(); - /* - * TODO [[rowheight]] This simply doesn't work with variable rows - * heights. - */ int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight()); setTopRowLogicalIndex(logicalLogical); @@ -3669,11 +3698,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * its parents are) removed from the document. Therefore, we sort * everything around that row instead. */ - final TableRowElement focusedRow = getEscalatorRowWithFocus(); + final TableRowElement focusedRow = getRowWithFocus(); if (focusedRow != null) { assert focusedRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body"; - assert visualRowOrder.contains(focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder."; + assert visualRowOrder.contains(focusedRow) + || body.spacerContainer.isSpacer(focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer."; } /* @@ -3694,6 +3724,34 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * the first child. */ + List<TableRowElement> orderedBodyRows = new ArrayList<TableRowElement>( + visualRowOrder); + Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer + .getSpacers(); + + /* + * Start at -1 to include a spacer that is rendered above the + * viewport, but its parent row is still not shown + */ + for (int i = -1; i < visualRowOrder.size(); i++) { + SpacerContainer.SpacerImpl spacer = spacers.remove(Integer + .valueOf(getTopRowLogicalIndex() + i)); + + if (spacer != null) { + orderedBodyRows.add(i + 1, spacer.getRootElement()); + spacer.show(); + } + } + /* + * At this point, invisible spacers aren't reordered, so their + * position in the DOM will remain undefined. + */ + + // If a spacer was not reordered, it means that it's out of view. + for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) { + unmovedSpacer.hide(); + } + /* * If we have a focused row, start in the mode where we put * everything underneath that row. Otherwise, all rows are placed as @@ -3701,8 +3759,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ boolean insertFirst = (focusedRow == null); - final ListIterator<TableRowElement> i = visualRowOrder - .listIterator(visualRowOrder.size()); + final ListIterator<TableRowElement> i = orderedBodyRows + .listIterator(orderedBodyRows.size()); while (i.hasPrevious()) { TableRowElement tr = i.previous(); @@ -3719,12 +3777,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** - * Get the escalator row that has focus. + * Get the {@literal <tbody>} row that contains (or has) focus. * - * @return The escalator row that contains a focused DOM element, or - * <code>null</code> if focus is outside of a body row. + * @return The {@literal <tbody>} row that contains a focused DOM + * element, or <code>null</code> if focus is outside of a body + * row. */ - private TableRowElement getEscalatorRowWithFocus() { + private TableRowElement getRowWithFocus() { TableRowElement rowContainingFocus = null; final Element focusedElement = WidgetUtil.getFocusedElement(); @@ -3759,6 +3818,99 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), cell.getElement()); } + + @Override + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + spacerContainer.setSpacer(rowIndex, height); + } + + @Override + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + spacerContainer.setSpacerUpdater(spacerUpdater); + } + + @Override + public SpacerUpdater getSpacerUpdater() { + return spacerContainer.getSpacerUpdater(); + } + + /** + * <em>Calculates</em> the correct top position of a row at a logical + * index, regardless if there is one there or not. + * <p> + * A correct result requires that both {@link #getDefaultRowHeight()} is + * consistent, and the placement and height of all spacers above the + * given logical index are consistent. + * + * @param logicalIndex + * the logical index of the row for which to calculate the + * top position + * @return the position at which to place a row in {@code logicalIndex} + * @see #getRowTop(TableRowElement) + */ + private double getRowTop(int logicalIndex) { + double top = spacerContainer + .getSpacerHeightsSumUntilIndex(logicalIndex); + return top + (logicalIndex * getDefaultRowHeight()); + } + + public void shiftRowPositions(int row, double diff) { + for (TableRowElement tr : getVisibleRowsAfter(row)) { + setRowPosition(tr, 0, getRowTop(tr) + diff); + } + } + + private List<TableRowElement> getVisibleRowsAfter(int logicalRow) { + Range visibleRowLogicalRange = getVisibleRowRange(); + + boolean allRowsAreInView = logicalRow < visibleRowLogicalRange + .getStart(); + boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange + .getEnd() - 1; + + if (allRowsAreInView) { + return Collections.unmodifiableList(visualRowOrder); + } else if (noRowsAreInView) { + return Collections.emptyList(); + } else { + int fromIndex = (logicalRow - visibleRowLogicalRange.getStart()) + 1; + int toIndex = visibleRowLogicalRange.length(); + List<TableRowElement> sublist = visualRowOrder.subList( + fromIndex, toIndex); + return Collections.unmodifiableList(sublist); + } + } + + /** + * This method calculates the current escalator row count directly from + * the DOM. + * <p> + * While Escalator is stable, this value should equal to + * {@link #visualRowOrder}.size(), but while row counts are being + * updated, these two values might differ for a short while. + * + * @return the actual DOM count of escalator rows + */ + public int getEscalatorRowCount() { + return root.getChildCount() + - spacerContainer.getSpacersInDom().size(); + } + + @Override + protected boolean rowCanBeFrozen(TableRowElement tr) { + return visualRowOrder.contains(tr); + } + + void reapplySpacerWidths() { + spacerContainer.reapplySpacerWidths(); + } + + void scrollToSpacer(int spacerIndex, ScrollDestination destination, + int padding) { + spacerContainer.scrollToSpacer(spacerIndex, destination, padding); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4430,6 +4582,957 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + /** + * A decision on how to measure a spacer when it is partially within a + * designated range. + * <p> + * The meaning of each value may differ depending on the context it is being + * used in. Check that particular method's JavaDoc. + */ + private enum SpacerInclusionStrategy { + /** A representation of "the entire spacer". */ + COMPLETE, + + /** A representation of "a partial spacer". */ + PARTIAL, + + /** A representation of "no spacer at all". */ + NONE + } + + private class SpacerContainer { + + /** This is used mainly for testing purposes */ + private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; + + private final class SpacerImpl implements Spacer { + private TableCellElement spacerElement; + private TableRowElement root; + private DivElement deco; + private int rowIndex; + private double height = -1; + private boolean domHasBeenSetup = false; + private double decoHeight; + + public SpacerImpl(int rowIndex) { + this.rowIndex = rowIndex; + + root = TableRowElement.as(DOM.createTR()); + spacerElement = TableCellElement.as(DOM.createTD()); + root.appendChild(spacerElement); + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + deco = DivElement.as(DOM.createDiv()); + } + + public void setPositionDiff(double x, double y) { + setPosition(getLeft() + x, getTop() + y); + } + + public void setupDom(double height) { + assert !domHasBeenSetup : "DOM can't be set up twice."; + assert RootPanel.get().getElement().isOrHasChild(root) : "Root element should've been attached to the DOM by now."; + domHasBeenSetup = true; + + getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX); + setHeight(height); + + spacerElement.setColSpan(getColumnConfiguration() + .getColumnCount()); + + setStylePrimaryName(getStylePrimaryName()); + } + + public TableRowElement getRootElement() { + return root; + } + + @Override + public Element getDecoElement() { + return deco; + } + + public void setPosition(double x, double y) { + positions.set(getRootElement(), x, y); + positions.set(getDecoElement(), 0, y); + } + + public void setStylePrimaryName(String style) { + UIObject.setStylePrimaryName(root, style + "-spacer"); + UIObject.setStylePrimaryName(deco, style + "-spacer-deco"); + } + + public void setHeight(double height) { + + assert height >= 0 : "Height must be more >= 0 (was " + height + + ")"; + + final double heightDiff = height - Math.max(0, this.height); + final double oldHeight = this.height; + + this.height = height; + root.getStyle().setHeight(height, Unit.PX); + + // move the visible spacers getRow row onwards. + shiftSpacerPositionsAfterRow(getRow(), heightDiff); + + /* + * If we're growing, we'll adjust the scroll size first, then + * adjust scrolling. If we're shrinking, we do it after the + * second if-clause. + */ + boolean spacerIsGrowing = heightDiff > 0; + if (spacerIsGrowing) { + verticalScrollbar.setScrollSize(verticalScrollbar + .getScrollSize() + heightDiff); + } + + /* + * Don't modify the scrollbars if we're expanding the -1 spacer + * while we're scrolled to the top. + */ + boolean minusOneSpacerException = spacerIsGrowing + && getRow() == -1 && body.getTopRowLogicalIndex() == 0; + + boolean viewportNeedsScrolling = getRow() < body + .getTopRowLogicalIndex() && !minusOneSpacerException; + if (viewportNeedsScrolling) { + + /* + * We can't use adjustScrollPos here, probably because of a + * bookkeeping-related race condition. + * + * This particular situation is easier, however, since we + * know exactly how many pixels we need to move (heightDiff) + * and all elements below the spacer always need to move + * that pixel amount. + */ + + for (TableRowElement row : body.visualRowOrder) { + body.setRowPosition(row, 0, body.getRowTop(row) + + heightDiff); + } + + double top = getTop(); + double bottom = top + oldHeight; + double scrollTop = verticalScrollbar.getScrollPos(); + + boolean viewportTopIsAtMidSpacer = top < scrollTop + && scrollTop < bottom; + + final double moveDiff; + if (viewportTopIsAtMidSpacer && !spacerIsGrowing) { + + /* + * If the scroll top is in the middle of the modified + * spacer, we want to scroll the viewport up as usual, + * but we don't want to scroll past the top of it. + * + * Math.max ensures this (remember: the result is going + * to be negative). + */ + + moveDiff = Math.max(heightDiff, top - scrollTop); + } else { + moveDiff = heightDiff; + } + body.setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + + moveDiff); + verticalScrollbar.setScrollPosByDelta(moveDiff); + + } else { + body.shiftRowPositions(getRow(), heightDiff); + } + + if (!spacerIsGrowing) { + verticalScrollbar.setScrollSize(verticalScrollbar + .getScrollSize() + heightDiff); + } + + updateDecoratorGeometry(height); + } + + /** Resizes and places the decorator. */ + private void updateDecoratorGeometry(double detailsHeight) { + Style style = deco.getStyle(); + decoHeight = detailsHeight + getBody().getDefaultRowHeight(); + + style.setTop( + -(getBody().getDefaultRowHeight() - getBorderTopHeight(getElement())), + Unit.PX); + style.setHeight(decoHeight, Unit.PX); + } + + private native double getBorderTopHeight(Element spacerCell) + /*-{ + if (typeof $wnd.getComputedStyle === 'function') { + var computedStyle = $wnd.getComputedStyle(spacerCell); + var borderTopWidth = computedStyle['borderTopWidth']; + var width = parseFloat(borderTopWidth); + return width; + } else { + var spacerRow = spacerCell.offsetParent; + var cloneCell = spacerCell.cloneNode(false); + spacerRow.appendChild(cloneCell); + cloneCell.style.height = "10px"; // IE8 wants the height to be set to something... + var heightWithBorder = cloneCell.offsetHeight; + cloneCell.style.borderTopWidth = "0"; + var heightWithoutBorder = cloneCell.offsetHeight; + spacerRow.removeChild(cloneCell); + + return heightWithBorder - heightWithoutBorder; + } + }-*/; + + @Override + public Element getElement() { + return spacerElement; + } + + @Override + public int getRow() { + return rowIndex; + } + + public double getHeight() { + assert height >= 0 : "Height was not previously set by setHeight."; + return height; + } + + public double getTop() { + return positions.getTop(getRootElement()); + } + + public double getLeft() { + return positions.getLeft(getRootElement()); + } + + /** + * Sets a new row index for this spacer. Also updates the bookeeping + * at {@link SpacerContainer#rowIndexToSpacer}. + */ + @SuppressWarnings("boxing") + public void setRowIndex(int rowIndex) { + SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex); + assert this == spacer : "trying to move an unexpected spacer."; + this.rowIndex = rowIndex; + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + rowIndexToSpacer.put(this.rowIndex, this); + } + + /** + * Updates the spacer's visibility parameters, based on whether it + * is being currently visible or not. + */ + public void updateVisibility() { + if (isInViewport()) { + show(); + } else { + hide(); + } + } + + private boolean isInViewport() { + int top = (int) Math.ceil(getTop()); + int height = (int) Math.floor(getHeight()); + Range location = Range.withLength(top, height); + return getViewportPixels().intersects(location); + } + + public void show() { + getRootElement().getStyle().clearDisplay(); + } + + public void hide() { + getRootElement().getStyle().setDisplay(Display.NONE); + } + + /** + * Crop the decorator element so that it doesn't overlap the header + * and footer sections. + * + * @param bodyTop + * the top cordinate of the escalator body + * @param bodyBottom + * the bottom cordinate of the escalator body + * @param decoWidth + * width of the deco + */ + private void updateDecoClip(final double bodyTop, + final double bodyBottom, final double decoWidth) { + final int top = deco.getAbsoluteTop(); + final int bottom = deco.getAbsoluteBottom(); + if (top < bodyTop || bottom > bodyBottom) { + final double topClip = Math.max(0.0D, bodyTop - top); + final double bottomClip = decoHeight + - Math.max(0.0D, bottom - bodyBottom); + final String clip = new StringBuilder("rect(") + .append(topClip).append("px,").append(decoWidth) + .append("px,").append(bottomClip).append("px,0") + .toString(); + deco.getStyle().setProperty("clip", clip); + } else { + deco.getStyle().clearProperty("clip"); + } + } + } + + private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<Integer, SpacerImpl>(); + + private SpacerUpdater spacerUpdater = SpacerUpdater.NULL; + + private final ScrollHandler spacerScroller = new ScrollHandler() { + private double prevScrollX = 0; + + @Override + public void onScroll(ScrollEvent event) { + if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) { + return; + } + + prevScrollX = getScrollLeft(); + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.setPosition(prevScrollX, spacer.getTop()); + } + } + }; + private HandlerRegistration spacerScrollerRegistration; + + /** Width of the spacers' decos. Calculated once then cached. */ + private double spacerDecoWidth = 0.0D; + + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + + if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) { + throw new IllegalArgumentException("invalid row index: " + + rowIndex + ", while the body only has " + + getBody().getRowCount() + " rows."); + } + + if (height >= 0) { + if (!spacerExists(rowIndex)) { + insertNewSpacer(rowIndex, height); + } else { + updateExistingSpacer(rowIndex, height); + } + } else if (spacerExists(rowIndex)) { + removeSpacer(rowIndex); + } + } + + /** Checks if a given element is a spacer element */ + public boolean isSpacer(TableRowElement focusedRow) { + + /* + * If this needs optimization, we could do a more heuristic check + * based on stylenames and stuff, instead of iterating through the + * map. + */ + + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + if (spacer.getRootElement().equals(focusedRow)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("boxing") + void scrollToSpacer(int spacerIndex, ScrollDestination destination, + int padding) { + + assert !destination.equals(ScrollDestination.MIDDLE) + || padding != 0 : "destination/padding check should be done before this method"; + + if (!rowIndexToSpacer.containsKey(spacerIndex)) { + throw new IllegalArgumentException("No spacer open at index " + + spacerIndex); + } + + SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex); + double targetStartPx = spacer.getTop(); + double targetEndPx = targetStartPx + spacer.getHeight(); + + Range viewportPixels = getViewportPixels(); + double viewportStartPx = viewportPixels.getStart(); + double viewportEndPx = viewportPixels.getEnd(); + + double scrollTop = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + setScrollTop(scrollTop); + } + + public void reapplySpacerWidths() { + // FIXME #16266 , spacers get couple pixels too much because borders + final double width = getInnerWidth() - spacerDecoWidth; + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.getRootElement().getStyle().setWidth(width, Unit.PX); + } + } + + public void paintRemoveSpacers(Range removedRowsRange) { + removeSpacers(removedRowsRange); + shiftSpacersByRows(removedRowsRange.getStart(), + -removedRowsRange.length()); + } + + @SuppressWarnings("boxing") + public void removeSpacers(Range removedRange) { + + Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer + .subMap(removedRange.getStart(), true, + removedRange.getEnd(), false); + + if (removedSpacers.isEmpty()) { + return; + } + + for (SpacerImpl spacer : removedSpacers.values()) { + /* + * [[optimization]] TODO: Each invocation of the setHeight + * method has a cascading effect in the DOM. if this proves to + * be slow, the DOM offset could be updated as a batch. + */ + + destroySpacerContent(spacer); + spacer.setHeight(0); // resets row offsets + spacer.getRootElement().removeFromParent(); + spacer.getDecoElement().removeFromParent(); + } + + removedSpacers.clear(); + + if (rowIndexToSpacer.isEmpty()) { + assert spacerScrollerRegistration != null : "Spacer scroller registration was null"; + spacerScrollerRegistration.removeHandler(); + spacerScrollerRegistration = null; + } + } + + public Map<Integer, SpacerImpl> getSpacers() { + return new HashMap<Integer, SpacerImpl>(rowIndexToSpacer); + } + + /** + * Calculates the sum of all spacers. + * + * @return sum of all spacers, or 0 if no spacers present + */ + public double getSpacerHeightsSum() { + return getHeights(rowIndexToSpacer.values()); + } + + /** + * Calculates the sum of all spacers from one row index onwards. + * + * @param logicalRowIndex + * the spacer to include as the first calculated spacer + * @return the sum of all spacers from {@code logicalRowIndex} and + * onwards, or 0 if no suitable spacers were found + */ + @SuppressWarnings("boxing") + public Collection<SpacerImpl> getSpacersForRowAndAfter( + int logicalRowIndex) { + return new ArrayList<SpacerImpl>(rowIndexToSpacer.tailMap( + logicalRowIndex, true).values()); + } + + /** + * Get all spacers from one pixel point onwards. + * <p> + * + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + * <dl> + * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + * <dd>include the spacer + * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + * <dd>include the spacer + * <dt>{@link SpacerInclusionStrategy#NONE NONE} + * <dd>ignore the spacer + * </dl> + * + * @param px + * the pixel point after which to return all spacers + * @param strategy + * the inclusion strategy regarding the {@code px} + * @return a collection of the spacers that exist after {@code px} + */ + public Collection<SpacerImpl> getSpacersAfterPx(final double px, + final SpacerInclusionStrategy strategy) { + + ArrayList<SpacerImpl> spacers = new ArrayList<SpacerImpl>( + rowIndexToSpacer.values()); + + for (int i = 0; i < spacers.size(); i++) { + SpacerImpl spacer = spacers.get(i); + + double top = spacer.getTop(); + double bottom = top + spacer.getHeight(); + + if (top > px) { + return spacers.subList(i, spacers.size()); + } else if (bottom > px) { + if (strategy == SpacerInclusionStrategy.NONE) { + return spacers.subList(i + 1, spacers.size()); + } else { + return spacers.subList(i, spacers.size()); + } + } + } + + return Collections.emptySet(); + } + + /** + * Gets the spacers currently rendered in the DOM. + * + * @return an unmodifiable (but live) collection of the spacers + * currently in the DOM + */ + public Collection<SpacerImpl> getSpacersInDom() { + return Collections + .unmodifiableCollection(rowIndexToSpacer.values()); + } + + /** + * Gets the amount of pixels occupied by spacers between two pixel + * points. + * <p> + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + * <dl> + * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + * <dd>take the entire spacer into account + * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + * <dd>take only the visible area into account + * <dt>{@link SpacerInclusionStrategy#NONE NONE} + * <dd>ignore that spacer + * </dl> + * + * @param rangeTop + * the top pixel point + * @param topInclusion + * the inclusion strategy regarding {@code rangeTop}. + * @param rangeBottom + * the bottom pixel point + * @param bottomInclusion + * the inclusion strategy regarding {@code rangeBottom}. + * @return the pixels occupied by spacers between {@code rangeTop} and + * {@code rangeBottom} + */ + public double getSpacerHeightsSumBetweenPx(double rangeTop, + SpacerInclusionStrategy topInclusion, double rangeBottom, + SpacerInclusionStrategy bottomInclusion) { + + assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom"; + + double heights = 0; + + /* + * TODO [[optimize]]: this might be somewhat inefficient (due to + * iterator-based scanning, instead of using the treemap's search + * functionalities). But it should be easy to write, read, verify + * and maintain. + */ + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + double top = spacer.getTop(); + double height = spacer.getHeight(); + double bottom = top + height; + + /* + * If we happen to implement a DoubleRange (in addition to the + * int-based Range) at some point, the following logic should + * probably be converted into using the + * Range.partitionWith-equivalent. + */ + + boolean topIsAboveRange = top < rangeTop; + boolean topIsInRange = rangeTop <= top && top <= rangeBottom; + boolean topIsBelowRange = rangeBottom < top; + + boolean bottomIsAboveRange = bottom < rangeTop; + boolean bottomIsInRange = rangeTop <= bottom + && bottom <= rangeBottom; + boolean bottomIsBelowRange = rangeBottom < bottom; + + assert topIsAboveRange ^ topIsBelowRange ^ topIsInRange : "Bad top logic"; + assert bottomIsAboveRange ^ bottomIsBelowRange + ^ bottomIsInRange : "Bad bottom logic"; + + if (bottomIsAboveRange) { + continue; + } else if (topIsBelowRange) { + return heights; + } + + else if (topIsAboveRange && bottomIsInRange) { + switch (topInclusion) { + case PARTIAL: + heights += bottom - rangeTop; + break; + case COMPLETE: + heights += height; + break; + default: + break; + } + } + + else if (topIsAboveRange && bottomIsBelowRange) { + + /* + * Here we arbitrarily decide that the top inclusion will + * have the honor of overriding the bottom inclusion if + * happens to be a conflict of interests. + */ + switch (topInclusion) { + case NONE: + return 0; + case COMPLETE: + return height; + case PARTIAL: + return rangeBottom - rangeTop; + default: + throw new IllegalArgumentException( + "Unexpected inclusion state :" + topInclusion); + } + + } else if (topIsInRange && bottomIsInRange) { + heights += height; + } + + else if (topIsInRange && bottomIsBelowRange) { + switch (bottomInclusion) { + case PARTIAL: + heights += rangeBottom - top; + break; + case COMPLETE: + heights += height; + break; + default: + break; + } + + return heights; + } + + else { + assert false : "Unnaccounted-for situation"; + } + } + + return heights; + } + + /** + * Gets the amount of pixels occupied by spacers from the top until a + * certain spot from the top of the body. + * + * @param px + * pixels counted from the top + * @return the pixels occupied by spacers up until {@code px} + */ + public double getSpacerHeightsSumUntilPx(double px) { + return getSpacerHeightsSumBetweenPx(0, + SpacerInclusionStrategy.PARTIAL, px, + SpacerInclusionStrategy.PARTIAL); + } + + /** + * Gets the amount of pixels occupied by spacers until a logical row + * index. + * + * @param logicalIndex + * a logical row index + * @return the pixels occupied by spacers up until {@code logicalIndex} + */ + @SuppressWarnings("boxing") + public double getSpacerHeightsSumUntilIndex(int logicalIndex) { + return getHeights(rowIndexToSpacer.headMap(logicalIndex, false) + .values()); + } + + private double getHeights(Collection<SpacerImpl> spacers) { + double heights = 0; + for (SpacerImpl spacer : spacers) { + heights += spacer.getHeight(); + } + return heights; + } + + /** + * Gets the height of the spacer for a row index. + * + * @param rowIndex + * the index of the row where the spacer should be + * @return the height of the spacer at index {@code rowIndex}, or 0 if + * there is no spacer there + */ + public double getSpacerHeight(int rowIndex) { + SpacerImpl spacer = getSpacer(rowIndex); + if (spacer != null) { + return spacer.getHeight(); + } else { + return 0; + } + } + + private boolean spacerExists(int rowIndex) { + return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex)); + } + + @SuppressWarnings("boxing") + private void insertNewSpacer(int rowIndex, double height) { + + if (spacerScrollerRegistration == null) { + spacerScrollerRegistration = addScrollHandler(spacerScroller); + } + + SpacerImpl spacer = new SpacerImpl(rowIndex); + + rowIndexToSpacer.put(rowIndex, spacer); + spacer.setPosition(getScrollLeft(), calculateSpacerTop(rowIndex)); + + TableRowElement spacerRoot = spacer.getRootElement(); + spacerRoot.getStyle().setWidth( + columnConfiguration.calculateRowWidth(), Unit.PX); + body.getElement().appendChild(spacerRoot); + spacer.setupDom(height); + + spacerDecoContainer.appendChild(spacer.getDecoElement()); + if (spacerDecoContainer.getParentElement() == null) { + getElement().appendChild(spacerDecoContainer); + // calculate the spacer deco width, it won't change + spacerDecoWidth = WidgetUtil + .getRequiredWidthBoundingClientRectDouble(spacer + .getDecoElement()); + } + + initSpacerContent(spacer); + + body.sortDomElements(); + } + + private void updateExistingSpacer(int rowIndex, double newHeight) { + getSpacer(rowIndex).setHeight(newHeight); + } + + public SpacerImpl getSpacer(int rowIndex) { + return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); + } + + private void removeSpacer(int rowIndex) { + removeSpacers(Range.withOnly(rowIndex)); + } + + public void setStylePrimaryName(String style) { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.setStylePrimaryName(style); + } + } + + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + if (spacerUpdater == null) { + throw new IllegalArgumentException( + "spacer updater cannot be null"); + } + + destroySpacerContent(rowIndexToSpacer.values()); + this.spacerUpdater = spacerUpdater; + initSpacerContent(rowIndexToSpacer.values()); + } + + public SpacerUpdater getSpacerUpdater() { + return spacerUpdater; + } + + private void destroySpacerContent(Iterable<SpacerImpl> spacers) { + for (SpacerImpl spacer : spacers) { + destroySpacerContent(spacer); + } + } + + private void destroySpacerContent(SpacerImpl spacer) { + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before detaching"; + spacerUpdater.destroy(spacer); + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before detaching"; + } + + private void initSpacerContent(Iterable<SpacerImpl> spacers) { + for (SpacerImpl spacer : spacers) { + initSpacerContent(spacer); + } + } + + private void initSpacerContent(SpacerImpl spacer) { + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before attaching"; + spacerUpdater.init(spacer); + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator during attaching"; + + spacer.updateVisibility(); + } + + public String getSubPartName(Element subElement) { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + if (spacer.getRootElement().isOrHasChild(subElement)) { + return "spacer[" + spacer.getRow() + "]"; + } + } + return null; + } + + public Element getSubPartElement(int index) { + SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index)); + if (spacer != null) { + return spacer.getElement(); + } else { + return null; + } + } + + private double calculateSpacerTop(int logicalIndex) { + return body.getRowTop(logicalIndex) + body.getDefaultRowHeight(); + } + + @SuppressWarnings("boxing") + private void shiftSpacerPositionsAfterRow(int changedRowIndex, + double diffPx) { + for (SpacerImpl spacer : rowIndexToSpacer.tailMap(changedRowIndex, + false).values()) { + spacer.setPositionDiff(0, diffPx); + } + } + + /** + * Shifts spacers at and after a specific row by an amount of rows. + * <p> + * This moves both their associated row index and also their visual + * placement. + * <p> + * <em>Note:</em> This method does not check for the validity of any + * arguments. + * + * @param index + * the index of first row to move + * @param numberOfRows + * the number of rows to shift the spacers with. A positive + * value is downwards, a negative value is upwards. + */ + public void shiftSpacersByRows(int index, int numberOfRows) { + final double pxDiff = numberOfRows * body.getDefaultRowHeight(); + for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter(index)) { + spacer.setPositionDiff(0, pxDiff); + spacer.setRowIndex(spacer.getRow() + numberOfRows); + } + } + + private void updateSpacerDecosVisibility() { + final Range visibleRowRange = getVisibleRowRange(); + Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer.subMap( + visibleRowRange.getStart() - 1, + visibleRowRange.getEnd() + 1).values(); + if (!visibleSpacers.isEmpty()) { + final double top = tableWrapper.getAbsoluteTop() + + header.getHeightOfSection(); + final double bottom = tableWrapper.getAbsoluteBottom() + - footer.getHeightOfSection(); + for (SpacerImpl spacer : visibleSpacers) { + spacer.updateDecoClip(top, bottom, spacerDecoWidth); + } + } + } + } + + private class ElementPositionBookkeeper { + /** + * A map containing cached values of an element's current top position. + */ + private final Map<Element, Double> elementTopPositionMap = new HashMap<Element, Double>(); + private final Map<Element, Double> elementLeftPositionMap = new HashMap<Element, Double>(); + + public void set(final Element e, final double x, final double y) { + assert e != null : "Element was null"; + position.set(e, x, y); + elementTopPositionMap.put(e, Double.valueOf(y)); + elementLeftPositionMap.put(e, Double.valueOf(x)); + } + + public double getTop(final Element e) { + Double top = elementTopPositionMap.get(e); + if (top == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return top.doubleValue(); + } + + public double getLeft(final Element e) { + Double left = elementLeftPositionMap.get(e); + if (left == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return left.doubleValue(); + } + + public void remove(Element e) { + elementTopPositionMap.remove(e); + elementLeftPositionMap.remove(e); + } + } + + public static class SubPartArguments { + private String type; + private int[] indices; + + private SubPartArguments(String type, int[] indices) { + /* + * The constructor is private so that no third party would by + * mistake start using this parsing scheme, since it's not official + * by TestBench (yet?). + */ + + this.type = type; + this.indices = indices; + } + + public String getType() { + return type; + } + + public int getIndicesLength() { + return indices.length; + } + + public int getIndex(int i) { + return indices[i]; + } + + public int[] getIndices() { + return Arrays.copyOf(indices, indices.length); + } + + static SubPartArguments create(String subPart) { + String[] splitArgs = subPart.split("\\["); + String type = splitArgs[0]; + int[] indices = new int[splitArgs.length - 1]; + for (int i = 0; i < indices.length; ++i) { + String tmp = splitArgs[i + 1]; + indices[i] = Integer + .parseInt(tmp.substring(0, tmp.length() - 1)); + } + return new SubPartArguments(type, indices); + } + } + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y /** * The solution to @@ -4489,7 +5592,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); private final HeaderRowContainer header = new HeaderRowContainer(headElem); - private final BodyRowContainer body = new BodyRowContainer(bodyElem); + private final BodyRowContainerImpl body = new BodyRowContainerImpl(bodyElem); private final FooterRowContainer footer = new FooterRowContainer(footElem); private final Scroller scroller = new Scroller(); @@ -4501,6 +5604,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .createDiv()); private final DivElement headerDeco = DivElement.as(DOM.createDiv()); private final DivElement footerDeco = DivElement.as(DOM.createDiv()); + private final DivElement spacerDecoContainer = DivElement.as(DOM + .createDiv()); private PositionFunction position; @@ -4528,6 +5633,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private final SubpixelBrowserBugDetector subpixelBrowserBugDetector = new SubpixelBrowserBugDetector(); + private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper(); + /** * Creates a new Escalator widget instance. */ @@ -4574,6 +5681,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker setStylePrimaryName("v-escalator"); + spacerDecoContainer.setAttribute("aria-hidden", "true"); + // init default dimensions setHeight(null); setWidth(null); @@ -4738,12 +5847,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * updated correctly. Since it isn't, we'll simply and brutally rip out * the DOM elements (in an elegant way, of course). */ - int rowsToRemove = bodyElem.getChildCount(); + int rowsToRemove = body.getEscalatorRowCount(); for (int i = 0; i < rowsToRemove; i++) { int index = rowsToRemove - i - 1; TableRowElement tr = bodyElem.getRows().getItem(index); body.paintRemoveRow(tr, index); - body.removeRowPosition(tr); + positions.remove(tr); } body.visualRowOrder.clear(); body.setTopRowLogicalIndex(0); @@ -4821,7 +5930,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * * @return the body. Never <code>null</code> */ - public RowContainer getBody() { + public BodyRowContainer getBody() { return body; } @@ -4940,6 +6049,28 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** + * Returns the scroll width for the escalator. Note that this is not + * necessary the same as {@code Element.scrollWidth} in the DOM. + * + * @since 7.5.0 + * @return the scroll width in pixels + */ + public double getScrollWidth() { + return horizontalScrollbar.getScrollSize(); + } + + /** + * Returns the scroll height for the escalator. Note that this is not + * necessary the same as {@code Element.scrollHeight} in the DOM. + * + * @since 7.5.0 + * @return the scroll height in pixels + */ + public double getScrollHeight() { + return verticalScrollbar.getScrollSize(); + } + + /** * Scrolls the body horizontally so that the column at the given index is * visible and there is at least {@code padding} pixels in the direction of * the given scroll destination. @@ -4956,15 +6087,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * column * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} - * and padding is nonzero, or if the indicated column is frozen + * and padding is nonzero; or if the indicated column is frozen; + * or if {@code destination == null} */ public void scrollToColumn(final int columnIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - if (destination == ScrollDestination.MIDDLE && padding != 0) { - throw new IllegalArgumentException( - "You cannot have a padding with a MIDDLE destination"); - } + validateScrollDestination(destination, padding); verifyValidColumnIndex(columnIndex); if (columnIndex < columnConfiguration.frozenColumns) { @@ -5000,15 +6129,14 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * if {@code rowIndex} is not a valid index for an existing row * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} - * and padding is nonzero + * and padding is nonzero; or if {@code destination == null} + * @see #scrollToRowAndSpacer(int, ScrollDestination, int) + * @see #scrollToSpacer(int, ScrollDestination, int) */ public void scrollToRow(final int rowIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - if (destination == ScrollDestination.MIDDLE && padding != 0) { - throw new IllegalArgumentException( - "You cannot have a padding with a MIDDLE destination"); - } + validateScrollDestination(destination, padding); verifyValidRowIndex(rowIndex); scroller.scrollToRow(rowIndex, destination, padding); @@ -5022,6 +6150,120 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** + * Scrolls the body vertically so that the spacer at the given row index is + * visible and there is at least {@literal padding} pixesl to the given + * scroll destination. + * + * @since + * @param spacerIndex + * the row index of the spacer to scroll to + * @param destination + * where the spacer should be aligned visually after scrolling + * @param padding + * the number of pixels to place between the scrolled-to spacer + * and the viewport edge + * @throws IllegalArgumentException + * if {@code spacerIndex} is not an opened spacer; or if + * {@code destination} is {@link ScrollDestination#MIDDLE} and + * padding is nonzero; or if {@code destination == null} + * @see #scrollToRow(int, ScrollDestination, int) + * @see #scrollToRowAndSpacer(int, ScrollDestination, int) + */ + public void scrollToSpacer(final int spacerIndex, + ScrollDestination destination, final int padding) + throws IllegalArgumentException { + validateScrollDestination(destination, padding); + body.scrollToSpacer(spacerIndex, destination, padding); + } + + /** + * Scrolls vertically to a row and the spacer below it. + * <p> + * If a spacer is not open at that index, this method behaves like + * {@link #scrollToRow(int, ScrollDestination, int)} + * + * @since + * @param rowIndex + * the index of the logical row to scroll to. -1 takes the + * topmost spacer into account as well. + * @param destination + * where the row should be aligned visually after scrolling + * @param padding + * the number pixels to place between the scrolled-to row and the + * viewport edge. + * @see #scrollToRow(int, ScrollDestination, int) + * @see #scrollToSpacer(int, ScrollDestination, int) + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and {@code padding} is not zero; or if {@code rowIndex} is + * not a valid row index, or -1; or if + * {@code destination == null}; or if {@code rowIndex == -1} and + * there is no spacer open at that index. + */ + public void scrollToRowAndSpacer(int rowIndex, + ScrollDestination destination, int padding) + throws IllegalArgumentException { + validateScrollDestination(destination, padding); + if (rowIndex != -1) { + verifyValidRowIndex(rowIndex); + } + + // row range + final Range rowRange; + if (rowIndex != -1) { + int rowTop = (int) Math.floor(body.getRowTop(rowIndex)); + int rowHeight = (int) Math.ceil(body.getDefaultRowHeight()); + rowRange = Range.withLength(rowTop, rowHeight); + } else { + rowRange = Range.withLength(0, 0); + } + + // get spacer + final SpacerContainer.SpacerImpl spacer = body.spacerContainer + .getSpacer(rowIndex); + + if (rowIndex == -1 && spacer == null) { + throw new IllegalArgumentException("Cannot scroll to row index " + + "-1, as there is no spacer open at that index."); + } + + // make into target range + final Range targetRange; + if (spacer != null) { + final int spacerTop = (int) Math.floor(spacer.getTop()); + final int spacerHeight = (int) Math.ceil(spacer.getHeight()); + Range spacerRange = Range.withLength(spacerTop, spacerHeight); + + targetRange = rowRange.combineWith(spacerRange); + } else { + targetRange = rowRange; + } + + // get params + int targetStart = targetRange.getStart(); + int targetEnd = targetRange.getEnd(); + double viewportStart = getScrollTop(); + double viewportEnd = viewportStart + body.getHeightOfSection(); + + double scrollPos = getScrollPos(destination, targetStart, targetEnd, + viewportStart, viewportEnd, padding); + + setScrollTop(scrollPos); + } + + private static void validateScrollDestination( + final ScrollDestination destination, final int padding) { + if (destination == null) { + throw new IllegalArgumentException("Destination cannot be null"); + } + + if (destination == ScrollDestination.MIDDLE && padding != 0) { + throw new IllegalArgumentException( + "You cannot have a padding with a MIDDLE destination"); + } + } + + /** * Recalculates the dimensions for all elements that require manual * calculations. Also updates the dimension caches. * <p> @@ -5048,6 +6290,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker scroller.recalculateScrollbarsForVirtualViewport(); body.verifyEscalatorCount(); + body.reapplySpacerWidths(); Profiler.leave("Escalator.recalculateElementSizes"); } @@ -5085,7 +6328,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker /** * Adds an event handler that gets notified when the range of visible rows - * changes e.g. because of scrolling or row resizing. + * changes e.g. because of scrolling, row resizing or spacers + * appearing/disappearing. * * @param rowVisibilityChangeHandler * the event handler @@ -5113,14 +6357,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** - * Gets the range of currently visible rows. + * Gets the logical index range of currently visible rows. * - * @return range of visible rows + * @return logical index range of visible rows */ public Range getVisibleRowRange() { if (!body.visualRowOrder.isEmpty()) { - return Range.withLength( - body.getLogicalRowIndex(body.visualRowOrder.getFirst()), + return Range.withLength(body.getTopRowLogicalIndex(), body.visualRowOrder.size()); } else { return Range.withLength(0, 0); @@ -5164,6 +6407,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco"); UIObject.setStylePrimaryName(horizontalScrollbarDeco, style + "-horizontal-scrollbar-deco"); + UIObject.setStylePrimaryName(spacerDecoContainer, style + + "-spacer-deco-container"); header.setStylePrimaryName(style); body.setStylePrimaryName(style); @@ -5222,8 +6467,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - double headerHeight = header.heightOfSection; - double footerHeight = footer.heightOfSection; + double headerHeight = header.getHeightOfSection(); + double footerHeight = footer.getHeightOfSection(); double bodyHeight = body.getDefaultRowHeight() * heightByRows; double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar .getScrollbarThickness() : 0; @@ -5416,4 +6661,181 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker columnConfiguration.getColumnWidth(i)); } } + + private Range getViewportPixels() { + int from = (int) Math.floor(verticalScrollbar.getScrollPos()); + int to = (int) body.getHeightOfSection(); + return Range.withLength(from, to); + } + + @Override + @SuppressWarnings("deprecation") + public com.google.gwt.user.client.Element getSubPartElement(String subPart) { + SubPartArguments args = parseSubPartArguments(subPart); + + Element tableStructureElement = getSubPartElementTableStructure(args); + if (tableStructureElement != null) { + return DOM.asOld(tableStructureElement); + } + + Element spacerElement = getSubPartElementSpacer(args); + if (spacerElement != null) { + return DOM.asOld(spacerElement); + } + + return null; + } + + private Element getSubPartElementTableStructure(SubPartArguments args) { + + String type = args.getType(); + int[] indices = args.getIndices(); + + // Get correct RowContainer for type from Escalator + RowContainer container = null; + if (type.equalsIgnoreCase("header")) { + container = getHeader(); + } else if (type.equalsIgnoreCase("cell")) { + // If wanted row is not visible, we need to scroll there. + Range visibleRowRange = getVisibleRowRange(); + if (indices.length > 0 && !visibleRowRange.contains(indices[0])) { + try { + scrollToRow(indices[0], ScrollDestination.ANY, 0); + } catch (IllegalArgumentException e) { + getLogger().log(Level.SEVERE, e.getMessage()); + } + // Scrolling causes a lazy loading event. No element can + // currently be retrieved. + return null; + } + container = getBody(); + } else if (type.equalsIgnoreCase("footer")) { + container = getFooter(); + } + + if (null != container) { + if (indices.length == 0) { + // No indexing. Just return the wanted container element + return container.getElement(); + } else { + try { + return getSubPart(container, indices); + } catch (Exception e) { + getLogger().log(Level.SEVERE, e.getMessage()); + } + } + } + return null; + } + + private Element getSubPart(RowContainer container, int[] indices) { + Element targetElement = container.getRowElement(indices[0]); + + // Scroll wanted column to view if able + if (indices.length > 1 && targetElement != null) { + if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) { + scrollToColumn(indices[1], ScrollDestination.ANY, 0); + } + + targetElement = getCellFromRow(TableRowElement.as(targetElement), + indices[1]); + + for (int i = 2; i < indices.length && targetElement != null; ++i) { + targetElement = (Element) targetElement.getChild(indices[i]); + } + } + + return targetElement; + } + + private static Element getCellFromRow(TableRowElement rowElement, int index) { + int childCount = rowElement.getCells().getLength(); + if (index < 0 || index >= childCount) { + return null; + } + + TableCellElement currentCell = null; + boolean indexInColspan = false; + int i = 0; + + while (!indexInColspan) { + currentCell = rowElement.getCells().getItem(i); + + // Calculate if this is the cell we are looking for + int colSpan = currentCell.getColSpan(); + indexInColspan = index < colSpan + i; + + // Increment by colspan to skip over hidden cells + i += colSpan; + } + return currentCell; + } + + private Element getSubPartElementSpacer(SubPartArguments args) { + if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) { + return body.spacerContainer.getSubPartElement(args.getIndex(0)); + } else { + return null; + } + } + + @Override + @SuppressWarnings("deprecation") + public String getSubPartName(com.google.gwt.user.client.Element subElement) { + + /* + * The spacer check needs to be before table structure check, because + * (for now) the table structure will take spacer elements into account + * as well, when it shouldn't. + */ + + String spacer = getSubPartNameSpacer(subElement); + if (spacer != null) { + return spacer; + } + + String tableStructure = getSubPartNameTableStructure(subElement); + if (tableStructure != null) { + return tableStructure; + } + + return null; + } + + private String getSubPartNameTableStructure(Element subElement) { + + List<RowContainer> containers = Arrays.asList(getHeader(), getBody(), + getFooter()); + List<String> containerType = Arrays.asList("header", "cell", "footer"); + + for (int i = 0; i < containers.size(); ++i) { + RowContainer container = containers.get(i); + boolean containerRow = (subElement.getTagName().equalsIgnoreCase( + "tr") && subElement.getParentElement() == container + .getElement()); + if (containerRow) { + /* + * Wanted SubPart is row that is a child of containers root to + * get indices, we use a cell that is a child of this row + */ + subElement = subElement.getFirstChildElement(); + } + + Cell cell = container.getCell(subElement); + if (cell != null) { + // Skip the column index if subElement was a child of root + return containerType.get(i) + "[" + cell.getRow() + + (containerRow ? "]" : "][" + cell.getColumn() + "]"); + } + } + return null; + } + + private String getSubPartNameSpacer(Element subElement) { + return body.spacerContainer.getSubPartName(subElement); + } + + public static SubPartArguments parseSubPartArguments(String subPart) { + return SubPartArguments.create(subPart); + } } diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 5eabbec621..6717f0c17d 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -25,7 +25,9 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,6 +38,8 @@ import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.EventTarget; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; @@ -45,6 +49,8 @@ import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyEvent; import com.google.gwt.event.dom.client.MouseEvent; import com.google.gwt.event.logical.shared.ValueChangeEvent; @@ -53,11 +59,17 @@ import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.touch.client.Point; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HasEnabled; import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; import com.google.gwt.user.client.ui.ResizeComposite; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; @@ -69,6 +81,7 @@ import com.vaadin.client.renderers.ComplexRenderer; import com.vaadin.client.renderers.Renderer; import com.vaadin.client.renderers.WidgetRenderer; import com.vaadin.client.ui.SubPartAware; +import com.vaadin.client.ui.dd.DragAndDropHandler; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; @@ -78,10 +91,16 @@ import com.vaadin.client.widget.escalator.RowContainer; import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction; +import com.vaadin.client.widget.escalator.Spacer; +import com.vaadin.client.widget.escalator.SpacerUpdater; +import com.vaadin.client.widget.grid.AutoScroller; +import com.vaadin.client.widget.grid.AutoScroller.AutoScrollerCallback; +import com.vaadin.client.widget.grid.AutoScroller.ScrollAxis; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; import com.vaadin.client.widget.grid.DataAvailableEvent; import com.vaadin.client.widget.grid.DataAvailableHandler; +import com.vaadin.client.widget.grid.DetailsGenerator; import com.vaadin.client.widget.grid.EditorHandler; import com.vaadin.client.widget.grid.EditorHandler.EditorRequest; import com.vaadin.client.widget.grid.EventCellReference; @@ -95,6 +114,10 @@ import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler; import com.vaadin.client.widget.grid.events.BodyKeyDownHandler; import com.vaadin.client.widget.grid.events.BodyKeyPressHandler; import com.vaadin.client.widget.grid.events.BodyKeyUpHandler; +import com.vaadin.client.widget.grid.events.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.FooterClickHandler; import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; @@ -127,7 +150,10 @@ import com.vaadin.client.widget.grid.sort.SortEvent; import com.vaadin.client.widget.grid.sort.SortHandler; import com.vaadin.client.widget.grid.sort.SortOrder; import com.vaadin.client.widgets.Escalator.AbstractRowContainer; +import com.vaadin.client.widgets.Escalator.SubPartArguments; import com.vaadin.client.widgets.Grid.Editor.State; +import com.vaadin.client.widgets.Grid.StaticSection.StaticCell; +import com.vaadin.client.widgets.Grid.StaticSection.StaticRow; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridStaticCellType; @@ -406,6 +432,16 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns <code>true</code> if this row contains spanned cells. + * + * @since 7.5.0 + * @return does this row contain spanned cells + */ + public boolean hasSpannedCells() { + return !cellGroups.isEmpty(); + } + + /** * Merges columns cells in a row * * @param columns @@ -420,6 +456,8 @@ public class Grid<T> extends ResizeComposite implements } HashSet<Column<?, ?>> columnGroup = new HashSet<Column<?, ?>>(); + // NOTE: this doesn't care about hidden columns, those are + // filtered in calculateColspans() for (Column<?, ?> column : columns) { if (!cells.containsKey(column)) { throw new IllegalArgumentException( @@ -483,39 +521,46 @@ public class Grid<T> extends ResizeComposite implements } void calculateColspans() { - // Reset all cells for (CELLTYPE cell : this.cells.values()) { cell.setColspan(1); } - - List<Column<?, ?>> columnOrder = new ArrayList<Column<?, ?>>( - section.grid.getColumns()); // Set colspan for grouped cells for (Set<Column<?, ?>> group : cellGroups.keySet()) { - if (!checkCellGroupAndOrder(columnOrder, group)) { + if (!checkMergedCellIsContinuous(group)) { + // on error simply break the merged cell cellGroups.get(group).setColspan(1); } else { - int colSpan = group.size(); - cellGroups.get(group).setColspan(colSpan); + int colSpan = 0; + for (Column<?, ?> column : group) { + if (!column.isHidden()) { + colSpan++; + } + } + // colspan can't be 0 + cellGroups.get(group).setColspan(Math.max(1, colSpan)); } } } - private boolean checkCellGroupAndOrder( - List<Column<?, ?>> columnOrder, Set<Column<?, ?>> cellGroup) { - if (!columnOrder.containsAll(cellGroup)) { + private boolean checkMergedCellIsContinuous( + Set<Column<?, ?>> mergedCell) { + // no matter if hidden or not, just check for continuous order + final List<Column<?, ?>> columnOrder = new ArrayList<Column<?, ?>>( + section.grid.getColumns()); + + if (!columnOrder.containsAll(mergedCell)) { return false; } for (int i = 0; i < columnOrder.size(); ++i) { - if (!cellGroup.contains(columnOrder.get(i))) { + if (!mergedCell.contains(columnOrder.get(i))) { continue; } - for (int j = 1; j < cellGroup.size(); ++j) { - if (!cellGroup.contains(columnOrder.get(i + j))) { + for (int j = 1; j < mergedCell.size(); ++j) { + if (!mergedCell.contains(columnOrder.get(i + j))) { return false; } } @@ -758,6 +803,14 @@ public class Grid<T> extends ResizeComposite implements assert grid != null; return grid; } + + protected void updateColSpans() { + for (ROWTYPE row : rows) { + if (row.hasSpannedCells()) { + row.calculateColspans(); + } + } + } } /** @@ -1426,7 +1479,7 @@ public class Grid<T> extends ResizeComposite implements cellWrapper.appendChild(cell); - Column<?, T> column = grid.getColumn(i); + Column<?, T> column = grid.getVisibleColumn(i); if (column.isEditable()) { Widget editor = getHandler().getWidget(column); if (editor != null) { @@ -1788,6 +1841,14 @@ public class Grid<T> extends ResizeComposite implements private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle"; + /** + * An initial height that is given to new details rows before rendering the + * appropriate widget that we then can be measure + * + * @see GridSpacerUpdater + */ + private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; + private EventCellReference<T> eventCell = new EventCellReference<T>(this); private GridKeyDownEvent keyDown = new GridKeyDownEvent(this, eventCell); private GridKeyUpEvent keyUp = new GridKeyUpEvent(this, eventCell); @@ -1880,28 +1941,33 @@ public class Grid<T> extends ResizeComposite implements /** * Sets the currently focused. + * <p> + * <em>NOTE:</em> the column index is the index in DOM, not the logical + * column index which includes hidden columns. * - * @param row + * @param rowIndex * the index of the row having focus - * @param column - * the index of the column having focus + * @param columnIndexDOM + * the index of the cell having focus * @param container * the row container having focus */ - private void setCellFocus(int row, int column, RowContainer container) { - if (row == rowWithFocus && cellFocusRange.contains(column) + private void setCellFocus(int rowIndex, int columnIndexDOM, + RowContainer container) { + if (rowIndex == rowWithFocus + && cellFocusRange.contains(columnIndexDOM) && container == this.containerWithFocus) { refreshRow(rowWithFocus); return; } int oldRow = rowWithFocus; - rowWithFocus = row; + rowWithFocus = rowIndex; Range oldRange = cellFocusRange; if (container == escalator.getBody()) { scrollToRow(rowWithFocus); - cellFocusRange = Range.withLength(column, 1); + cellFocusRange = Range.withLength(columnIndexDOM, 1); } else { int i = 0; Element cell = container.getRowElement(rowWithFocus) @@ -1910,7 +1976,7 @@ public class Grid<T> extends ResizeComposite implements int colSpan = cell .getPropertyInt(FlyweightCell.COLSPAN_ATTR); Range cellRange = Range.withLength(i, colSpan); - if (cellRange.contains(column)) { + if (cellRange.contains(columnIndexDOM)) { cellFocusRange = cellRange; break; } @@ -1918,10 +1984,12 @@ public class Grid<T> extends ResizeComposite implements ++i; } while (cell != null); } - - if (column >= escalator.getColumnConfiguration() + int columnIndex = getColumns().indexOf( + getVisibleColumn(columnIndexDOM)); + if (columnIndex >= escalator.getColumnConfiguration() .getFrozenColumnCount()) { - escalator.scrollToColumn(column, ScrollDestination.ANY, 10); + escalator.scrollToColumn(columnIndexDOM, ScrollDestination.ANY, + 10); } if (this.containerWithFocus == container) { @@ -1967,7 +2035,7 @@ public class Grid<T> extends ResizeComposite implements * a cell object */ public void setCellFocus(CellReference<T> cell) { - setCellFocus(cell.getRowIndex(), cell.getColumnIndex(), + setCellFocus(cell.getRowIndex(), cell.getColumnIndexDOM(), escalator.findRowContainer(cell.getElement())); } @@ -2001,7 +2069,7 @@ public class Grid<T> extends ResizeComposite implements --newRow; break; case KeyCodes.KEY_RIGHT: - if (cellFocusRange.getEnd() >= getColumns().size()) { + if (cellFocusRange.getEnd() >= getVisibleColumns().size()) { return; } newColumn = cellFocusRange.getEnd(); @@ -2485,7 +2553,7 @@ public class Grid<T> extends ResizeComposite implements private boolean columnsAreGuaranteedToBeWiderThanGrid() { double freeSpace = escalator.getInnerWidth(); - for (Column<?, ?> column : getColumns()) { + for (Column<?, ?> column : getVisibleColumns()) { if (column.getWidth() >= 0) { freeSpace -= column.getWidth(); } else if (column.getMinimumWidth() >= 0) { @@ -2501,7 +2569,7 @@ public class Grid<T> extends ResizeComposite implements /* Step 1: Apply all column widths as they are. */ Map<Integer, Double> selfWidths = new LinkedHashMap<Integer, Double>(); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (int index = 0; index < columns.size(); index++) { selfWidths.put(index, columns.get(index).getWidth()); } @@ -2542,6 +2610,7 @@ public class Grid<T> extends ResizeComposite implements final Set<Column<?, T>> columnsToExpand = new HashSet<Column<?, T>>(); List<Column<?, T>> nonFixedColumns = new ArrayList<Column<?, T>>(); Map<Integer, Double> columnSizes = new HashMap<Integer, Double>(); + final List<Column<?, T>> visibleColumns = getVisibleColumns(); /* * Set all fixed widths and also calculate the size-to-fit widths @@ -2550,7 +2619,7 @@ public class Grid<T> extends ResizeComposite implements * This way we know with how many pixels we have left to expand the * rest. */ - for (Column<?, T> column : getColumns()) { + for (Column<?, T> column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; final double widthFixed = Math.max(widthAsIs, @@ -2559,11 +2628,11 @@ public class Grid<T> extends ResizeComposite implements && (column.getExpandRatio() == -1 || column == selectionColumn); if (isFixedWidth) { - columnSizes.put(indexOfColumn(column), widthFixed); + columnSizes.put(visibleColumns.indexOf(column), widthFixed); reservedPixels += widthFixed; } else { nonFixedColumns.add(column); - columnSizes.put(indexOfColumn(column), -1.0d); + columnSizes.put(visibleColumns.indexOf(column), -1.0d); } } @@ -2581,7 +2650,7 @@ public class Grid<T> extends ResizeComposite implements columnsToExpand.add(column); } reservedPixels += newWidth; - columnSizes.put(indexOfColumn(column), newWidth); + columnSizes.put(visibleColumns.indexOf(column), newWidth); } /* @@ -2609,8 +2678,8 @@ public class Grid<T> extends ResizeComposite implements final Column<?, T> column = i.next(); final int expandRatio = getExpandRatio(column, defaultExpandRatios); - final double autoWidth = columnSizes - .get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + final double autoWidth = columnSizes.get(columnIndex); final double maxWidth = getMaxWidth(column); double expandedWidth = autoWidth + widthPerRatio * expandRatio; @@ -2620,7 +2689,7 @@ public class Grid<T> extends ResizeComposite implements totalRatios -= expandRatio; aColumnHasMaxedOut = true; pixelsToDistribute -= maxWidth - autoWidth; - columnSizes.put(indexOfColumn(column), maxWidth); + columnSizes.put(columnIndex, maxWidth); } } } while (aColumnHasMaxedOut); @@ -2656,13 +2725,14 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : columnsToExpand) { final int expandRatio = getExpandRatio(column, defaultExpandRatios); - final double autoWidth = columnSizes.get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + final double autoWidth = columnSizes.get(columnIndex); double totalWidth = autoWidth + widthPerRatio * expandRatio; if (leftOver > 0) { totalWidth += 1; leftOver--; } - columnSizes.put(indexOfColumn(column), totalWidth); + columnSizes.put(columnIndex, totalWidth); totalRatios -= expandRatio; } @@ -2683,7 +2753,7 @@ public class Grid<T> extends ResizeComposite implements * remove those pixels from other columns */ double pixelsToRemoveFromOtherColumns = 0; - for (Column<?, T> column : getColumns()) { + for (Column<?, T> column : visibleColumns) { /* * We can't iterate over columnsToExpand, even though that * would be convenient. This is because some column without @@ -2692,11 +2762,11 @@ public class Grid<T> extends ResizeComposite implements */ double minWidth = getMinWidth(column); - double currentWidth = columnSizes - .get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + double currentWidth = columnSizes.get(columnIndex); boolean hasAutoWidth = column.getWidth() < 0; if (hasAutoWidth && currentWidth < minWidth) { - columnSizes.put(indexOfColumn(column), minWidth); + columnSizes.put(columnIndex, minWidth); pixelsToRemoveFromOtherColumns += (minWidth - currentWidth); minWidthsCausedReflows = true; @@ -2722,7 +2792,7 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : columnsToExpand) { final double pixelsToRemove = pixelsToRemovePerRatio * getExpandRatio(column, defaultExpandRatios); - int colIndex = indexOfColumn(column); + int colIndex = visibleColumns.indexOf(column); columnSizes.put(colIndex, columnSizes.get(colIndex) - pixelsToRemove); } @@ -2789,6 +2859,431 @@ public class Grid<T> extends ResizeComposite implements } } + private class GridSpacerUpdater implements SpacerUpdater { + + private static final String STRIPE_CLASSNAME = "stripe"; + + private final Map<Element, Widget> elementToWidgetMap = new HashMap<Element, Widget>(); + + @Override + public void init(Spacer spacer) { + initTheming(spacer); + + int rowIndex = spacer.getRow(); + + Widget detailsWidget = null; + try { + detailsWidget = detailsGenerator.getDetails(rowIndex); + } catch (Throwable e) { + getLogger().log( + Level.SEVERE, + "Exception while generating details for row " + + rowIndex, e); + } + + final double spacerHeight; + Element spacerElement = spacer.getElement(); + if (detailsWidget == null) { + spacerElement.removeAllChildren(); + spacerHeight = DETAILS_ROW_INITIAL_HEIGHT; + } else { + Element element = detailsWidget.getElement(); + spacerElement.appendChild(element); + setParent(detailsWidget, Grid.this); + Widget previousWidget = elementToWidgetMap.put(element, + detailsWidget); + + assert previousWidget == null : "Overwrote a pre-existing widget on row " + + rowIndex + " without proper removal first."; + + /* + * Once we have the content properly inside the DOM, we should + * re-measure it to make sure that it's the correct height. + */ + double measuredHeight = WidgetUtil + .getRequiredHeightBoundingClientRectDouble(spacerElement); + assert getElement().isOrHasChild(spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be."; + spacerHeight = measuredHeight; + } + + escalator.getBody().setSpacer(rowIndex, spacerHeight); + } + + @Override + public void destroy(Spacer spacer) { + Element spacerElement = spacer.getElement(); + + assert getElement().isOrHasChild(spacerElement) : "Trying " + + "to destroy a spacer that is not connected to this " + + "Grid's DOM. (row: " + spacer.getRow() + ", element: " + + spacerElement + ")"; + + Widget detailsWidget = elementToWidgetMap.remove(spacerElement + .getFirstChildElement()); + + if (detailsWidget != null) { + /* + * The widget may be null here if the previous generator + * returned a null widget. + */ + + assert spacerElement.getFirstChild() != null : "The " + + "details row to destroy did not contain a widget - " + + "probably removed by something else without " + + "permission? (row: " + spacer.getRow() + + ", element: " + spacerElement + ")"; + + setParent(detailsWidget, null); + spacerElement.removeAllChildren(); + } + } + + private void initTheming(Spacer spacer) { + Element spacerRoot = spacer.getElement(); + + if (spacer.getRow() % 2 == 1) { + spacerRoot.getParentElement().addClassName(STRIPE_CLASSNAME); + } else { + spacerRoot.getParentElement().removeClassName(STRIPE_CLASSNAME); + } + } + + } + + /** + * Sidebar displaying toggles for hidable columns and custom widgets + * provided by the application. + * <p> + * The button for opening the sidebar is automatically visible inside the + * grid, if it contains any column hiding options or custom widgets. The + * column hiding toggles and custom widgets become visible once the sidebar + * has been opened. + * + * @since 7.5.0 + */ + private static class Sidebar extends Composite { + + private final ClickHandler openCloseButtonHandler = new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + if (!isOpen()) { + open(); + } else { + close(); + } + } + }; + + private final FlowPanel rootContainer; + + private final FlowPanel content; + + private final MenuBar menuBar; + + private final Button openCloseButton; + + private final Grid<?> grid; + + private Sidebar(Grid<?> grid) { + this.grid = grid; + + rootContainer = new FlowPanel(); + initWidget(rootContainer); + + openCloseButton = new Button(); + openCloseButton.addClickHandler(openCloseButtonHandler); + + rootContainer.add(openCloseButton); + + content = new FlowPanel() { + @Override + public boolean remove(Widget w) { + // Check here to catch child.removeFromParent() calls + boolean removed = super.remove(w); + if (removed) { + updateVisibility(); + } + + return removed; + } + }; + + menuBar = new MenuBar(true) { + + @Override + public MenuItem addItem(MenuItem item) { + if (getParent() == null) { + content.insert(this, 0); + updateVisibility(); + } + return super.addItem(item); + } + + @Override + public void removeItem(MenuItem item) { + super.removeItem(item); + if (getItems().isEmpty()) { + menuBar.removeFromParent(); + } + } + + @Override + public void onBrowserEvent(Event event) { + // selecting a item with enter will lose the focus and + // selected item, which means that further keyboard + // selection won't work unless we do this: + if (event.getTypeInt() == Event.ONKEYDOWN + && event.getKeyCode() == KeyCodes.KEY_ENTER) { + final MenuItem item = getSelectedItem(); + super.onBrowserEvent(event); + Scheduler.get().scheduleDeferred( + new ScheduledCommand() { + + @Override + public void execute() { + selectItem(item); + focus(); + } + }); + + } else { + super.onBrowserEvent(event); + } + } + + }; + KeyDownHandler keyDownHandler = new KeyDownHandler() { + + @Override + public void onKeyDown(KeyDownEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { + close(); + } + } + }; + openCloseButton.addDomHandler(keyDownHandler, + KeyDownEvent.getType()); + menuBar.addDomHandler(keyDownHandler, KeyDownEvent.getType()); + } + + /** + * Opens the sidebar if not yet opened. Opening the sidebar has no + * effect if it is empty. + */ + public void open() { + if (!isOpen() && isInDOM()) { + addStyleName("opened"); + removeStyleName("closed"); + rootContainer.add(content); + } + openCloseButton.setHeight(""); + } + + /** + * Closes the sidebar if not yet closed. + */ + public void close() { + if (isOpen()) { + removeStyleName("opened"); + addStyleName("closed"); + content.removeFromParent(); + // adjust open button to header height when closed + setHeightToHeaderCellHeight(); + } + } + + /** + * Returns whether the sidebar is open or not. + * + * @return <code>true</code> if open, <code>false</code> if not + */ + public boolean isOpen() { + return content.getParent() == rootContainer; + } + + /** + * Adds or moves the given widget to the end of the sidebar. + * + * @param widget + * the widget to add or move + */ + public void add(Widget widget) { + content.add(widget); + updateVisibility(); + } + + /** + * Removes the given widget from the sidebar. + * + * @param widget + * the widget to remove + */ + public void remove(Widget widget) { + content.remove(widget); + // updateVisibility is called by remove listener + } + + /** + * Inserts given widget to the given index inside the sidebar. If the + * widget is already in the sidebar, then it is moved to the new index. + * <p> + * See + * {@link FlowPanel#insert(com.google.gwt.user.client.ui.IsWidget, int)} + * for further details. + * + * @param widget + * the widget to insert + * @param beforeIndex + * 0-based index position for the widget. + */ + public void insert(Widget widget, int beforeIndex) { + content.insert(widget, beforeIndex); + updateVisibility(); + } + + @Override + public void setStylePrimaryName(String styleName) { + super.setStylePrimaryName(styleName); + content.setStylePrimaryName(styleName + "-content"); + openCloseButton.setStylePrimaryName(styleName + "-button"); + if (isOpen()) { + addStyleName("open"); + removeStyleName("closed"); + } else { + removeStyleName("open"); + addStyleName("closed"); + } + } + + private void setHeightToHeaderCellHeight() { + try { + double height = WidgetUtil + .getRequiredHeightBoundingClientRectDouble(grid.escalator + .getHeader().getRowElement(0) + .getFirstChildElement()) + - (WidgetUtil.measureVerticalBorder(getElement()) / 2); + openCloseButton.setHeight(height + "px"); + } catch (NullPointerException npe) { + getLogger() + .warning( + "Got null header first row or first row cell when calculating sidebar button height"); + openCloseButton.setHeight(grid.escalator.getHeader() + .getDefaultRowHeight() + "px"); + } + } + + private void updateVisibility() { + final boolean hasWidgets = content.getWidgetCount() > 0; + final boolean isVisible = isInDOM(); + if (isVisible && !hasWidgets) { + Grid.setParent(this, null); + getElement().removeFromParent(); + } else if (!isVisible && hasWidgets) { + close(); + grid.getElement().appendChild(getElement()); + Grid.setParent(this, grid); + // border calculation won't work until attached + setHeightToHeaderCellHeight(); + } + } + + private boolean isInDOM() { + return getParent() != null; + } + } + + /** + * UI and functionality related to hiding columns with toggles in the + * sidebar. + */ + private final class ColumnHider { + + /** Map from columns to their hiding toggles, component might change */ + private HashMap<Column<?, T>, MenuItem> columnToHidingToggleMap = new HashMap<Grid.Column<?, T>, MenuItem>(); + + /** + * When column is being hidden with a toggle, do not refresh toggles for + * no reason. Also helps for keeping the keyboard navigation working. + */ + private boolean hidingColumn; + + private void updateColumnHidable(final Column<?, T> column) { + if (column.isHidable()) { + MenuItem toggle = columnToHidingToggleMap.get(column); + if (toggle == null) { + toggle = createToggle(column); + } + toggle.setStyleName("hidden", column.isHidden()); + } else if (columnToHidingToggleMap.containsKey(column)) { + sidebar.menuBar.removeItem((columnToHidingToggleMap + .remove(column))); + } + updateTogglesOrder(); + } + + private MenuItem createToggle(final Column<?, T> column) { + MenuItem toggle = new MenuItem(createHTML(column), true, + new ScheduledCommand() { + + @Override + public void execute() { + hidingColumn = true; + column.setHidden(!column.isHidden(), true); + hidingColumn = false; + } + }); + toggle.addStyleName("column-hiding-toggle"); + columnToHidingToggleMap.put(column, toggle); + return toggle; + } + + private String createHTML(Column<?, T> column) { + final StringBuffer buf = new StringBuffer(); + buf.append("<span class=\""); + if (column.isHidden()) { + buf.append("v-off"); + } else { + buf.append("v-on"); + } + buf.append("\"><div>"); + String caption = column.getHidingToggleCaption(); + if (caption == null) { + caption = column.headerCaption; + } + buf.append(caption); + buf.append("</div></span>"); + + return buf.toString(); + } + + private void updateTogglesOrder() { + if (!hidingColumn) { + for (Column<?, T> column : getColumns()) { + if (column.isHidable()) { + final MenuItem menuItem = columnToHidingToggleMap + .get(column); + sidebar.menuBar.removeItem(menuItem); + sidebar.menuBar.addItem(menuItem); + } + } + } + } + + private void updateHidingToggle(Column<?, T> column) { + if (column.isHidable()) { + MenuItem toggle = columnToHidingToggleMap.get(column); + toggle.setHTML(createHTML(column)); + toggle.setStyleName("hidden", column.isHidden()); + } // else we can just ignore + } + + private void removeColumnHidingToggle(Column<?, T> column) { + sidebar.menuBar.removeItem(columnToHidingToggleMap.get(column)); + } + + } + /** * Escalator used internally by grid to render the rows */ @@ -2798,6 +3293,8 @@ public class Grid<T> extends ResizeComposite implements private final Footer footer = GWT.create(Footer.class); + private final Sidebar sidebar = new Sidebar(this); + /** * List of columns in the grid. Order defines the visible order. */ @@ -2874,6 +3371,442 @@ public class Grid<T> extends ResizeComposite implements private boolean enabled = true; + private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; + private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater(); + /** A set keeping track of the indices of all currently open details */ + private Set<Integer> visibleDetails = new HashSet<Integer>(); + + private boolean columnReorderingAllowed; + + private ColumnHider columnHider = new ColumnHider(); + + private DragAndDropHandler dndHandler = new DragAndDropHandler(); + + private AutoScroller autoScroller = new AutoScroller(this); + + private DragAndDropHandler.DragAndDropCallback headerCellDndCallback = new DragAndDropHandler.DragAndDropCallback() { + + private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { + + @Override + public void onAutoScroll(int scrollDiff) { + autoScrollX = scrollDiff; + onDragUpdate(null); + } + + @Override + public void onAutoScrollReachedMin() { + // make sure the drop marker is visible on the left + autoScrollX = 0; + updateDragDropMarker(clientX); + } + + @Override + public void onAutoScrollReachedMax() { + // make sure the drop marker is visible on the right + autoScrollX = 0; + updateDragDropMarker(clientX); + } + }; + /** + * Elements for displaying the dragged column(s) and drop marker + * properly + */ + private Element table; + private Element tableHeader; + /** Marks the column drop location */ + private Element dropMarker; + /** A copy of the dragged column(s), moves with cursor. */ + private Element dragElement; + /** Tracks index of the column whose left side the drop would occur */ + private int latestColumnDropIndex; + /** + * Map of possible drop positions for the column and the corresponding + * column index. + */ + private final TreeMap<Double, Integer> possibleDropPositions = new TreeMap<Double, Integer>(); + /** + * Makes sure that drag cancel doesn't cause anything unwanted like sort + */ + private HandlerRegistration columnSortPreventRegistration; + + private int clientX; + + /** How much the grid is being auto scrolled while dragging. */ + private int autoScrollX; + + /** Captures the value of the focused column before reordering */ + private int focusedColumnIndex; + + private void initHeaderDragElementDOM() { + if (table == null) { + tableHeader = DOM.createTHead(); + dropMarker = DOM.createDiv(); + tableHeader.appendChild(dropMarker); + table = DOM.createTable(); + table.appendChild(tableHeader); + table.setClassName("header-drag-table"); + } + // update the style names on each run in case primary name has been + // modified + tableHeader.setClassName(escalator.getHeader().getElement() + .getClassName()); + dropMarker.setClassName(getStylePrimaryName() + "-drop-marker"); + int topOffset = 0; + for (int i = 0; i < eventCell.getRowIndex(); i++) { + topOffset += escalator.getHeader().getRowElement(i) + .getFirstChildElement().getOffsetHeight(); + } + tableHeader.getStyle().setTop(topOffset, Unit.PX); + + getElement().appendChild(table); + } + + @Override + public void onDragUpdate(NativePreviewEvent event) { + if (event != null) { + clientX = WidgetUtil.getTouchOrMouseClientX(event + .getNativeEvent()); + autoScrollX = 0; + } + resolveDragElementHorizontalPosition(clientX); + updateDragDropMarker(clientX); + } + + private void updateDragDropMarker(final int clientX) { + final double scrollLeft = getScrollLeft(); + final double cursorXCoordinate = clientX + - escalator.getHeader().getElement().getAbsoluteLeft(); + final Entry<Double, Integer> cellEdgeOnRight = possibleDropPositions + .ceilingEntry(cursorXCoordinate); + final Entry<Double, Integer> cellEdgeOnLeft = possibleDropPositions + .floorEntry(cursorXCoordinate); + final double diffToRightEdge = cellEdgeOnRight == null ? Double.MAX_VALUE + : cellEdgeOnRight.getKey() - cursorXCoordinate; + final double diffToLeftEdge = cellEdgeOnLeft == null ? Double.MAX_VALUE + : cursorXCoordinate - cellEdgeOnLeft.getKey(); + + double dropMarkerLeft = 0 - scrollLeft; + if (diffToRightEdge > diffToLeftEdge) { + latestColumnDropIndex = cellEdgeOnLeft.getValue(); + dropMarkerLeft += cellEdgeOnLeft.getKey(); + } else { + latestColumnDropIndex = cellEdgeOnRight.getValue(); + dropMarkerLeft += cellEdgeOnRight.getKey(); + } + + dropMarkerLeft += autoScrollX; + + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (dropMarkerLeft < frozenColumnsWidth + || dropMarkerLeft > escalator.getHeader().getElement() + .getOffsetWidth() || dropMarkerLeft < 0) { + dropMarkerLeft = -10000000; + } + dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); + } + + private void resolveDragElementHorizontalPosition(final int clientX) { + double left = clientX - table.getAbsoluteLeft(); + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (left < frozenColumnsWidth) { + left = (int) frozenColumnsWidth; + } + + // do not show the drag element beyond a spanned header cell + // limitation + final Double leftBound = possibleDropPositions.firstKey(); + final Double rightBound = possibleDropPositions.lastKey(); + double scrollLeft = getScrollLeft(); + if (left + scrollLeft < leftBound) { + left = leftBound - scrollLeft + autoScrollX; + } else if (left + scrollLeft > rightBound) { + left = rightBound - scrollLeft + autoScrollX; + } + + // do not show the drag element beyond the grid + left = Math.max(0, Math.min(left, table.getClientWidth())); + + left -= dragElement.getClientWidth() / 2; + dragElement.getStyle().setLeft(left, Unit.PX); + } + + @Override + public boolean onDragStart(NativeEvent startingEvent) { + calculatePossibleDropPositions(); + + if (possibleDropPositions.isEmpty()) { + return false; + } + + initHeaderDragElementDOM(); + // needs to clone focus and sorting indicators too (UX) + dragElement = DOM.clone(eventCell.getElement(), true); + dragElement.getStyle().clearWidth(); + dropMarker.getStyle().setProperty("height", + dragElement.getStyle().getHeight()); + tableHeader.appendChild(dragElement); + // mark the column being dragged for styling + eventCell.getElement().addClassName("dragged"); + // mark the floating cell, for styling & testing + dragElement.addClassName("dragged-column-header"); + + // start the auto scroll handler + autoScroller.setScrollArea(60); + autoScroller.start(startingEvent, ScrollAxis.HORIZONTAL, + autoScrollerCallback); + return true; + } + + @Override + public void onDragEnd() { + table.removeFromParent(); + dragElement.removeFromParent(); + eventCell.getElement().removeClassName("dragged"); + } + + @Override + public void onDrop() { + final int draggedColumnIndex = eventCell.getColumnIndex(); + final int colspan = header.getRow(eventCell.getRowIndex()) + .getCell(eventCell.getColumn()).getColspan(); + if (latestColumnDropIndex != draggedColumnIndex + && latestColumnDropIndex != (draggedColumnIndex + colspan)) { + List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> reordered = new ArrayList<Column<?, T>>(); + if (draggedColumnIndex < latestColumnDropIndex) { + reordered.addAll(columns.subList(0, draggedColumnIndex)); + reordered.addAll(columns.subList(draggedColumnIndex + + colspan, latestColumnDropIndex)); + reordered.addAll(columns.subList(draggedColumnIndex, + draggedColumnIndex + colspan)); + reordered.addAll(columns.subList(latestColumnDropIndex, + columns.size())); + } else { + reordered.addAll(columns.subList(0, latestColumnDropIndex)); + reordered.addAll(columns.subList(draggedColumnIndex, + draggedColumnIndex + colspan)); + reordered.addAll(columns.subList(latestColumnDropIndex, + draggedColumnIndex)); + reordered.addAll(columns.subList(draggedColumnIndex + + colspan, columns.size())); + } + reordered.remove(selectionColumn); // since setColumnOrder will + // add it anyway! + + // capture focused cell column before reorder + Cell focusedCell = cellFocusHandler.getFocusedCell(); + if (focusedCell != null) { + // take hidden columns into account + focusedColumnIndex = getColumns().indexOf( + getVisibleColumn(focusedCell.getColumn())); + } + + Column<?, T>[] array = reordered.toArray(new Column[reordered + .size()]); + setColumnOrder(array); + transferCellFocusOnDrop(); + } // else no reordering + } + + private void transferCellFocusOnDrop() { + final Cell focusedCell = cellFocusHandler.getFocusedCell(); + if (focusedCell != null) { + final int focusedColumnIndexDOM = focusedCell.getColumn(); + final int focusedRowIndex = focusedCell.getRow(); + final int draggedColumnIndex = eventCell.getColumnIndex(); + // transfer focus if it was effected by the new column order + final RowContainer rowContainer = escalator + .findRowContainer(focusedCell.getElement()); + if (focusedColumnIndex == draggedColumnIndex) { + // move with the dragged column + int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1 + : latestColumnDropIndex; + // remove hidden columns from indexing + adjustedDropIndex = getVisibleColumns().indexOf( + getColumn(adjustedDropIndex)); + cellFocusHandler.setCellFocus(focusedRowIndex, + adjustedDropIndex, rowContainer); + } else if (latestColumnDropIndex <= focusedColumnIndex + && draggedColumnIndex > focusedColumnIndex) { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedColumnIndexDOM + 1, rowContainer); + } else if (latestColumnDropIndex > focusedColumnIndex + && draggedColumnIndex < focusedColumnIndex) { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedColumnIndexDOM - 1, rowContainer); + } + } + } + + @Override + public void onDragCancel() { + // cancel next click so that we may prevent column sorting if + // mouse was released on top of the dragged cell + if (columnSortPreventRegistration == null) { + columnSortPreventRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent( + NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONCLICK) { + event.cancel(); + event.getNativeEvent().preventDefault(); + columnSortPreventRegistration + .removeHandler(); + columnSortPreventRegistration = null; + } + } + }); + } + autoScroller.stop(); + } + + private double getFrozenColumnsWidth() { + double value = getMultiSelectColumnWidth(); + for (int i = 0; i < getFrozenColumnCount(); i++) { + value += getColumn(i).getWidthActual(); + } + return value; + } + + private double getMultiSelectColumnWidth() { + if (getSelectionModel().getSelectionColumnRenderer() != null) { + // frozen checkbox column is present, it is always the first + // column + return escalator.getHeader().getElement() + .getFirstChildElement().getFirstChildElement() + .getOffsetWidth(); + } + return 0.0; + } + + /** + * Returns the amount of frozen columns. The selection column is always + * considered frozen, since it can't be moved. + */ + private int getSelectionAndFrozenColumnCount() { + // no matter if selection column is frozen or not, it is considered + // frozen for column dnd reorder + if (getSelectionModel().getSelectionColumnRenderer() != null) { + return Math.max(0, getFrozenColumnCount()) + 1; + } else { + return Math.max(0, getFrozenColumnCount()); + } + } + + @SuppressWarnings("boxing") + private void calculatePossibleDropPositions() { + possibleDropPositions.clear(); + + final int draggedColumnIndex = eventCell.getColumnIndex(); + final StaticRow<?> draggedCellRow = header.getRow(eventCell + .getRowIndex()); + final int draggedColumnRightIndex = draggedColumnIndex + + draggedCellRow.getCell(eventCell.getColumn()) + .getColspan(); + final int frozenColumns = getSelectionAndFrozenColumnCount(); + final Range draggedCellRange = Range.between(draggedColumnIndex, + draggedColumnRightIndex); + /* + * If the dragged cell intersects with a spanned cell in any other + * header or footer row, then the drag is limited inside that + * spanned cell. The same rules apply: the cell can't be dropped + * inside another spanned cell. The left and right bounds keep track + * of the edges of the most limiting spanned cell. + */ + int leftBound = -1; + int rightBound = getColumnCount() + 1; + + final HashSet<Integer> unavailableColumnDropIndices = new HashSet<Integer>(); + final List<StaticRow<?>> rows = new ArrayList<StaticRow<?>>(); + rows.addAll(header.getRows()); + rows.addAll(footer.getRows()); + for (StaticRow<?> row : rows) { + if (!row.hasSpannedCells()) { + continue; + } + final boolean isDraggedCellRow = row.equals(draggedCellRow); + for (int cellColumnIndex = frozenColumns; cellColumnIndex < getColumnCount(); cellColumnIndex++) { + StaticCell cell = row.getCell(getColumn(cellColumnIndex)); + int colspan = cell.getColspan(); + if (colspan <= 1) { + continue; + } + final int cellColumnRightIndex = cellColumnIndex + colspan; + final Range cellRange = Range.between(cellColumnIndex, + cellColumnRightIndex); + final boolean intersects = draggedCellRange + .intersects(cellRange); + if (intersects && !isDraggedCellRow) { + // if the currently iterated cell is inside or same as + // the dragged cell, then it doesn't restrict the drag + if (cellRange.isSubsetOf(draggedCellRange)) { + cellColumnIndex = cellColumnRightIndex - 1; + continue; + } + /* + * if the dragged cell is a spanned cell and it crosses + * with the currently iterated cell without sharing + * either start or end then not possible to drag the + * cell. + */ + if (!draggedCellRange.isSubsetOf(cellRange)) { + return; + } + // the spanned cell overlaps the dragged cell (but is + // not the dragged cell) + if (cellColumnIndex <= draggedColumnIndex + && cellColumnIndex > leftBound) { + leftBound = cellColumnIndex; + } + if (cellColumnRightIndex < rightBound) { + rightBound = cellColumnRightIndex; + } + cellColumnIndex = cellColumnRightIndex - 1; + } + + else { // can't drop inside a spanned cell, or this is the + // dragged cell + while (colspan > 1) { + cellColumnIndex++; + colspan--; + unavailableColumnDropIndices.add(cellColumnIndex); + } + } + } + } + + if (leftBound == (rightBound - 1)) { + return; + } + + double position = getFrozenColumnsWidth(); + // iterate column indices and add possible drop positions + for (int i = frozenColumns; i < getColumnCount(); i++) { + Column<?, T> column = getColumn(i); + if (!unavailableColumnDropIndices.contains(i) + && !column.isHidden()) { + if (leftBound != -1) { + if (i >= leftBound && i <= rightBound) { + possibleDropPositions.put(position, i); + } + } else { + possibleDropPositions.put(position, i); + } + } + position += column.getWidthActual(); + } + + if (leftBound == -1) { + // add the right side of the last column as columns.size() + possibleDropPositions.put(position, getColumnCount()); + } + } + + }; + /** * Enumeration for easy setting of selection mode. */ @@ -2976,8 +3909,14 @@ public class Grid<T> extends ResizeComposite implements private boolean editable = true; + private boolean hidden = false; + + private boolean hidable = false; + private String headerCaption = ""; + private String hidingToggleCaption = null; + private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; private double maximumWidthPx = GridConstants.DEFAULT_MAX_WIDTH; private int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO; @@ -3086,6 +4025,9 @@ public class Grid<T> extends ResizeComposite implements HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { row.getCell(this).setText(headerCaption); + if (isHidable()) { + grid.columnHider.updateHidingToggle(this); + } } } @@ -3148,6 +4090,9 @@ public class Grid<T> extends ResizeComposite implements * This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. + * <p> + * If the column is currently {@link #isHidden() hidden}, then this set + * width has effect only once the column has been made visible again. * * @param pixels * the width in pixels or negative for auto sizing @@ -3155,14 +4100,17 @@ public class Grid<T> extends ResizeComposite implements public Column<C, T> setWidth(double pixels) { if (!WidgetUtil.pixelValuesEqual(widthUser, pixels)) { widthUser = pixels; - scheduleColumnWidthRecalculator(); + if (!isHidden()) { + scheduleColumnWidthRecalculator(); + } } return this; } void doSetWidth(double pixels) { + assert !isHidden() : "applying width for a hidden column"; if (grid != null) { - int index = grid.columns.indexOf(this); + int index = grid.getVisibleColumns().indexOf(this); ColumnConfiguration conf = grid.escalator .getColumnConfiguration(); conf.setColumnWidth(index, pixels); @@ -3174,6 +4122,9 @@ public class Grid<T> extends ResizeComposite implements * <p> * <em>Note:</em> If a negative value was given to * {@link #setWidth(double)}, that same negative value is returned here. + * <p> + * <em>Note:</em> Returns the value, even if the column is currently + * {@link #isHidden() hidden}. * * @return pixel width of the column, or a negative number if the column * width has been automatically calculated. @@ -3188,13 +4139,18 @@ public class Grid<T> extends ResizeComposite implements * Returns the effective pixel width of the column. * <p> * This differs from {@link #getWidth()} only when the column has been - * automatically resized. + * automatically resized, or when the column is currently + * {@link #isHidden() hidden}, when the value is 0. * * @return pixel width of the column. */ public double getWidthActual() { + if (isHidden()) { + return 0; + } return grid.escalator.getColumnConfiguration() - .getColumnWidthActual(grid.columns.indexOf(this)); + .getColumnWidthActual( + grid.getVisibleColumns().indexOf(this)); } void reapplyWidth() { @@ -3231,6 +4187,129 @@ public class Grid<T> extends ResizeComposite implements return sortable; } + /** + * 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 + */ + public void setHidden(boolean hidden) { + setHidden(hidden, false); + } + + private void setHidden(boolean hidden, boolean userOriginated) { + if (this.hidden != hidden) { + if (hidden) { + grid.escalator.getColumnConfiguration().removeColumns( + grid.getVisibleColumns().indexOf(this), 1); + this.hidden = hidden; + } else { + this.hidden = hidden; + + final int columnIndex = grid.getVisibleColumns().indexOf( + this); + grid.escalator.getColumnConfiguration().insertColumns( + columnIndex, 1); + + // make sure column is set to frozen if it needs to be, + // escalator doesn't handle situation where the added column + // would be the last frozen column + int gridFrozenColumns = grid.getFrozenColumnCount(); + int escalatorFrozenColumns = grid.escalator + .getColumnConfiguration().getFrozenColumnCount(); + if (gridFrozenColumns > escalatorFrozenColumns + && escalatorFrozenColumns == columnIndex) { + grid.escalator.getColumnConfiguration() + .setFrozenColumnCount(++escalatorFrozenColumns); + } + } + grid.columnHider.updateHidingToggle(this); + grid.header.updateColSpans(); + grid.footer.updateColSpans(); + scheduleColumnWidthRecalculator(); + this.grid.fireEvent(new ColumnVisibilityChangeEvent<T>(this, + hidden, userOriginated)); + } + } + + /** + * 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 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> if the user can hide this column, + * <code>false</code> if not + */ + public void setHidable(boolean hidable) { + if (this.hidable != hidable) { + this.hidable = hidable; + grid.columnHider.updateColumnHidable(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 hidable; + } + + /** + * Sets the hiding toggle's caption for this column. Shown in the toggle + * for this column in the grid's sidebar when the column is + * {@link #isHidable() hidable}. + * <p> + * Defaults to <code>null</code>, when will use whatever is set with + * {@link #setHeaderCaption(String)}. + * + * @since + * @param hidingToggleCaption + * the caption for the hiding toggle for this column + */ + public void setHidingToggleCaption(String hidingToggleCaption) { + this.hidingToggleCaption = hidingToggleCaption; + if (isHidable()) { + grid.columnHider.updateHidingToggle(this); + } + } + + /** + * Gets the hiding toggle caption for this column. + * + * @since + * @see #setHidingToggleCaption(String) + * @return the hiding toggle's caption for this column + */ + public String getHidingToggleCaption() { + return hidingToggleCaption; + } + @Override public String toString() { String details = ""; @@ -3464,8 +4543,9 @@ public class Grid<T> extends ResizeComposite implements Renderer<?> renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { + Column<?, T> column = getVisibleColumn(cell.getColumn()); rendererCellReference.set(cell, - getColumn(cell.getColumn())); + getColumns().indexOf(column), column); ((ComplexRenderer<?>) renderer) .init(rendererCellReference); } catch (RuntimeException e) { @@ -3561,7 +4641,8 @@ public class Grid<T> extends ResizeComposite implements cellFocusHandler.updateFocusedRowStyle(row); for (FlyweightCell cell : cellsToUpdate) { - Column<?, T> column = getColumn(cell.getColumn()); + Column<?, T> column = getVisibleColumn(cell.getColumn()); + final int columnIndex = getColumns().indexOf(column); assert column != null : "Column was not found from cell (" + cell.getColumn() + "," + cell.getRow() + ")"; @@ -3571,7 +4652,8 @@ public class Grid<T> extends ResizeComposite implements if (hasData && cellStyleGenerator != null) { try { - cellReference.set(cell.getColumn(), column); + cellReference + .set(cell.getColumn(), columnIndex, column); String generatedStyle = cellStyleGenerator .getStyle(cellReference); setCustomStyleName(cell.getElement(), generatedStyle); @@ -3588,7 +4670,7 @@ public class Grid<T> extends ResizeComposite implements Renderer renderer = column.getRenderer(); try { - rendererCellReference.set(cell, column); + rendererCellReference.set(cell, columnIndex, column); if (renderer instanceof ComplexRenderer) { // Hide cell content if needed ComplexRenderer clxRenderer = (ComplexRenderer) renderer; @@ -3662,8 +4744,9 @@ public class Grid<T> extends ResizeComposite implements Renderer renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { + Column<?, T> column = getVisibleColumn(cell.getColumn()); rendererCellReference.set(cell, - getColumn(cell.getColumn())); + getColumns().indexOf(column), column); ((ComplexRenderer) renderer) .destroy(rendererCellReference); } catch (RuntimeException e) { @@ -3692,7 +4775,7 @@ public class Grid<T> extends ResizeComposite implements @Override public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) { StaticSection.StaticRow<?> staticRow = section.getRow(row.getRow()); - final List<Column<?, T>> columns = getColumns(); + final List<Column<?, T>> columns = getVisibleColumns(); setCustomStyleName(row.getElement(), staticRow.getStyleName()); @@ -3740,7 +4823,7 @@ public class Grid<T> extends ResizeComposite implements cleanup(cell); - Column<?, ?> column = getColumn(cell.getColumn()); + Column<?, ?> column = getVisibleColumn(cell.getColumn()); SortOrder sortingOrder = getSortOrder(column); if (!headerRow.isDefault() || !column.isSortable() || sortingOrder == null) { @@ -3813,7 +4896,7 @@ public class Grid<T> extends ResizeComposite implements @Override public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) { StaticSection.StaticRow<?> gridRow = section.getRow(row.getRow()); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (FlyweightCell cell : attachedCells) { StaticSection.StaticCell metadata = gridRow.getCell(columns @@ -3843,7 +4926,7 @@ public class Grid<T> extends ResizeComposite implements if (section.getRowCount() > row.getRow()) { StaticSection.StaticRow<?> gridRow = section.getRow(row .getRow()); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (FlyweightCell cell : cellsToDetach) { StaticSection.StaticCell metadata = gridRow.getCell(columns .get(cell.getColumn())); @@ -3892,6 +4975,8 @@ public class Grid<T> extends ResizeComposite implements setSelectionMode(SelectionMode.SINGLE); + escalator.getBody().setSpacerUpdater(gridSpacerUpdater); + escalator.addScrollHandler(new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { @@ -3980,6 +5065,8 @@ public class Grid<T> extends ResizeComposite implements super.setStylePrimaryName(style); escalator.setStylePrimaryName(style); editor.setStylePrimaryName(style); + sidebar.setStylePrimaryName(style + "-sidebar"); + sidebar.addStyleName("v-contextmenu"); String rowStyle = getStylePrimaryName() + "-row"; rowHasDataStyleName = rowStyle + "-has-data"; @@ -4156,6 +5243,10 @@ public class Grid<T> extends ResizeComposite implements Set<String> events = new HashSet<String>(); events.addAll(getConsumedEventsForRenderer(column.getRenderer())); + if (column.isHidable()) { + columnHider.updateColumnHidable(column); + } + sinkEvents(events); } @@ -4179,7 +5270,7 @@ public class Grid<T> extends ResizeComposite implements } private Renderer<?> findRenderer(FlyweightCell cell) { - Column<?, T> column = getColumn(cell.getColumn()); + Column<?, T> column = getVisibleColumn(cell.getColumn()); assert column != null : "Could not find column at index:" + cell.getColumn(); return column.getRenderer(); @@ -4204,7 +5295,8 @@ public class Grid<T> extends ResizeComposite implements int columnIndex = columns.indexOf(column); // Remove from column configuration - escalator.getColumnConfiguration().removeColumns(columnIndex, 1); + escalator.getColumnConfiguration().removeColumns( + getVisibleColumns().indexOf(column), 1); updateFrozenColumns(); @@ -4215,10 +5307,16 @@ public class Grid<T> extends ResizeComposite implements ((Column<?, T>) column).setGrid(null); columns.remove(columnIndex); + + if (column.isHidable()) { + columnHider.removeColumnHidingToggle(column); + } } /** * Returns the amount of columns in the grid. + * <p> + * <em>NOTE:</em> this includes the hidden columns in the count. * * @return The number of columns in the grid */ @@ -4227,7 +5325,9 @@ public class Grid<T> extends ResizeComposite implements } /** - * Returns a list of columns in the grid. + * Returns a list columns in the grid, including hidden columns. + * <p> + * For currently visible columns, use {@link #getVisibleColumns()}. * * @return A unmodifiable list of the columns in the grid */ @@ -4237,7 +5337,27 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns a list of the currently visible columns in the grid. + * <p> + * No {@link Column#isHidden() hidden} columns included. + * + * @since 7.5.0 + * @return A unmodifiable list of the currently visible columns in the grid + */ + public List<Column<?, T>> getVisibleColumns() { + ArrayList<Column<?, T>> visible = new ArrayList<Column<?, T>>(); + for (Column<?, T> c : columns) { + if (!c.isHidden()) { + visible.add(c); + } + } + return Collections.unmodifiableList(visible); + } + + /** * Returns a column by its index in the grid. + * <p> + * <em>NOTE:</em> The indexing includes hidden columns. * * @param index * the index of the column @@ -4252,15 +5372,13 @@ public class Grid<T> extends ResizeComposite implements return columns.get(index); } - /** - * Returns current index of given column - * - * @param column - * column in grid - * @return column index, or <code>-1</code> if not in this Grid - */ - protected int indexOfColumn(Column<?, T> column) { - return columns.indexOf(column); + private Column<?, T> getVisibleColumn(int index) + throws IllegalArgumentException { + List<Column<?, T>> visibleColumns = getVisibleColumns(); + if (index < 0 || index >= visibleColumns.size()) { + throw new IllegalStateException("Column not found."); + } + return visibleColumns.get(index); } /** @@ -4708,13 +5826,21 @@ public class Grid<T> extends ResizeComposite implements + getColumnCount() + ")"); } - this.frozenColumnCount = numberOfColumns; + frozenColumnCount = numberOfColumns; updateFrozenColumns(); } private void updateFrozenColumns() { int numberOfColumns = frozenColumnCount; + // for the escalator the hidden columns are not in the frozen column + // count, but for grid they are. thus need to convert the index + for (int i = 0; i < frozenColumnCount; i++) { + if (getColumn(i).isHidden()) { + numberOfColumns--; + } + } + if (numberOfColumns == -1) { numberOfColumns = 0; } else if (selectionColumn != null) { @@ -4730,6 +5856,9 @@ public class Grid<T> extends ResizeComposite implements * 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 includes {@link Column#isHidden() hidden columns} in + * the count. * * @return the number of frozen columns */ @@ -4749,6 +5878,9 @@ public class Grid<T> extends ResizeComposite implements /** * Scrolls to a certain row, using {@link ScrollDestination#ANY}. + * <p> + * If the details for that row are visible, those will be taken into account + * as well. * * @param rowIndex * zero-based index of the row to scroll to. @@ -4763,6 +5895,9 @@ public class Grid<T> extends ResizeComposite implements /** * Scrolls to a certain row, using user-specified scroll destination. + * <p> + * If the details for that row are visible, those will be taken into account + * as well. * * @param rowIndex * zero-based index of the row to scroll to. @@ -4782,6 +5917,9 @@ public class Grid<T> extends ResizeComposite implements /** * Scrolls to a certain row using only user-specified parameters. + * <p> + * If the details for that row are visible, those will be taken into account + * as well. * * @param rowIndex * zero-based index of the row to scroll to. @@ -4811,7 +5949,7 @@ public class Grid<T> extends ResizeComposite implements + ") is above maximum (" + maxsize + ")!"); } - escalator.scrollToRow(rowIndex, destination, paddingPx); + escalator.scrollToRowAndSpacer(rowIndex, destination, paddingPx); } /** @@ -4849,6 +5987,17 @@ public class Grid<T> extends ResizeComposite implements } /** + * Sets the horizontal scroll offset + * + * @since 7.5.0 + * @param px + * the number of pixels this grid should be scrolled right + */ + public void setScrollLeft(double px) { + escalator.setScrollLeft(px); + } + + /** * Gets the horizontal scroll offset * * @return the number of pixels this grid is scrolled to the right @@ -4857,6 +6006,26 @@ public class Grid<T> extends ResizeComposite implements return escalator.getScrollLeft(); } + /** + * Returns the height of the scrollable area in pixels. + * + * @since 7.5.0 + * @return the height of the scrollable area in pixels + */ + public double getScrollHeight() { + return escalator.getScrollHeight(); + } + + /** + * Returns the width of the scrollable area in pixels. + * + * @since 7.5.0 + * @return the width of the scrollable area in pixels. + */ + public double getScrollWidth() { + return escalator.getScrollWidth(); + } + private static final Logger getLogger() { return Logger.getLogger(Grid.class.getName()); } @@ -4960,7 +6129,7 @@ public class Grid<T> extends ResizeComposite implements EventTarget target = event.getEventTarget(); - if (!Element.is(target)) { + if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) { return; } @@ -5007,6 +6176,10 @@ public class Grid<T> extends ResizeComposite implements if (!isElementInChildWidget(e)) { + if (handleHeaderCellDragStartEvent(event, container)) { + return; + } + // Sorting through header Click / KeyUp if (handleHeaderDefaultRowEvent(event, container)) { return; @@ -5026,6 +6199,21 @@ public class Grid<T> extends ResizeComposite implements } } + private boolean isOrContainsInSpacer(Node node) { + Node n = node; + while (n != null && n != getElement()) { + boolean isElement = Element.is(n); + if (isElement) { + String className = Element.as(n).getClassName(); + if (className.contains(getStylePrimaryName() + "-spacer")) { + return true; + } + } + n = n.getParentNode(); + } + return false; + } + private boolean isElementInChildWidget(Element e) { Widget w = WidgetUtil.findWidget(e, null); @@ -5155,6 +6343,31 @@ public class Grid<T> extends ResizeComposite implements return true; } + private boolean handleHeaderCellDragStartEvent(Event event, + RowContainer container) { + if (!isColumnReorderingAllowed()) { + return false; + } + if (container != escalator.getHeader()) { + return false; + } + if (eventCell.getColumnIndex() < escalator.getColumnConfiguration() + .getFrozenColumnCount()) { + return false; + } + + if (event.getTypeInt() == Event.ONMOUSEDOWN + && event.getButton() == NativeEvent.BUTTON_LEFT + || event.getTypeInt() == Event.ONTOUCHSTART) { + dndHandler.onDragStartOnDraggableElement(event, + headerCellDndCallback); + event.preventDefault(); + event.stopPropagation(); + return true; + } + return false; + } + private Point rowEventTouchStartingPoint; private CellStyleGenerator<T> cellStyleGenerator; private RowStyleGenerator<T> rowStyleGenerator; @@ -5253,153 +6466,82 @@ public class Grid<T> extends ResizeComposite implements } @Override + @SuppressWarnings("deprecation") public com.google.gwt.user.client.Element getSubPartElement(String subPart) { - // Parse SubPart string to type and indices - String[] splitArgs = subPart.split("\\["); - - String type = splitArgs[0]; - int[] indices = new int[splitArgs.length - 1]; - for (int i = 0; i < indices.length; ++i) { - String tmp = splitArgs[i + 1]; - indices[i] = Integer.parseInt(tmp.substring(0, tmp.length() - 1)); - } - - // Get correct RowContainer for type from Escalator - RowContainer container = null; - if (type.equalsIgnoreCase("header")) { - container = escalator.getHeader(); - } else if (type.equalsIgnoreCase("cell")) { - // If wanted row is not visible, we need to scroll there. - Range visibleRowRange = escalator.getVisibleRowRange(); - if (indices.length > 0 && !visibleRowRange.contains(indices[0])) { - try { - scrollToRow(indices[0]); - } catch (IllegalArgumentException e) { - getLogger().log(Level.SEVERE, e.getMessage()); - } - // Scrolling causes a lazy loading event. No element can - // currently be retrieved. - return null; - } - container = escalator.getBody(); - } else if (type.equalsIgnoreCase("footer")) { - container = escalator.getFooter(); - } else if (type.equalsIgnoreCase("editor")) { - if (editor.getState() != State.ACTIVE) { - // Editor is not there. - return null; - } - if (indices.length == 0) { - return DOM.asOld(editor.editorOverlay); - } else if (indices.length == 1 && indices[0] < columns.size()) { - escalator.scrollToColumn(indices[0], ScrollDestination.ANY, 0); - return editor.getWidget(columns.get(indices[0])).getElement(); - } else { - return null; - } - } + /* + * handles details[] (translated to spacer[] for Escalator), cell[], + * header[] and footer[] + */ + Element escalatorElement = escalator.getSubPartElement(subPart + .replaceFirst("^details\\[", "spacer[")); - if (null != container) { - if (indices.length == 0) { - // No indexing. Just return the wanted container element - return DOM.asOld(container.getElement()); - } else { - try { - return DOM.asOld(getSubPart(container, indices)); - } catch (Exception e) { - getLogger().log(Level.SEVERE, e.getMessage()); - } - } + if (escalatorElement != null) { + return DOM.asOld(escalatorElement); } - return null; - } - - private Element getSubPart(RowContainer container, int[] indices) { - Element targetElement = container.getRowElement(indices[0]); - - // Scroll wanted column to view if able - if (indices.length > 1 && targetElement != null) { - if (escalator.getColumnConfiguration().getFrozenColumnCount() <= indices[1]) { - escalator.scrollToColumn(indices[1], ScrollDestination.ANY, 0); - } - targetElement = getCellFromRow(TableRowElement.as(targetElement), - indices[1]); + SubPartArguments args = Escalator.parseSubPartArguments(subPart); - for (int i = 2; i < indices.length && targetElement != null; ++i) { - targetElement = (Element) targetElement.getChild(indices[i]); - } + Element editor = getSubPartElementEditor(args); + if (editor != null) { + return DOM.asOld(editor); } - return targetElement; + return null; } - private Element getCellFromRow(TableRowElement rowElement, int index) { - int childCount = rowElement.getCells().getLength(); - if (index < 0 || index >= childCount) { + private Element getSubPartElementEditor(SubPartArguments args) { + + if (!args.getType().equalsIgnoreCase("editor") + || editor.getState() != State.ACTIVE) { return null; } - TableCellElement currentCell = null; - boolean indexInColspan = false; - int i = 0; - - while (!indexInColspan) { - currentCell = rowElement.getCells().getItem(i); - - // Calculate if this is the cell we are looking for - int colSpan = currentCell.getColSpan(); - indexInColspan = index < colSpan + i; - - // Increment by colspan to skip over hidden cells - i += colSpan; + if (args.getIndicesLength() == 0) { + return editor.editorOverlay; + } else if (args.getIndicesLength() == 1 + && args.getIndex(0) < columns.size()) { + escalator + .scrollToColumn(args.getIndex(0), ScrollDestination.ANY, 0); + return editor.getWidget(columns.get(args.getIndex(0))).getElement(); } - return currentCell; + + return null; } @Override + @SuppressWarnings("deprecation") public String getSubPartName(com.google.gwt.user.client.Element subElement) { - // Containers and matching SubPart types - List<RowContainer> containers = Arrays.asList(escalator.getHeader(), - escalator.getBody(), escalator.getFooter()); - List<String> containerType = Arrays.asList("header", "cell", "footer"); - for (int i = 0; i < containers.size(); ++i) { - RowContainer container = containers.get(i); - boolean containerRow = (subElement.getTagName().equalsIgnoreCase( - "tr") && subElement.getParentElement() == container - .getElement()); - if (containerRow) { - // Wanted SubPart is row that is a child of containers root - // To get indices, we use a cell that is a child of this row - subElement = DOM.asOld(subElement.getFirstChildElement()); - } + String escalatorStructureName = escalator.getSubPartName(subElement); + if (escalatorStructureName != null) { + return escalatorStructureName.replaceFirst("^spacer", "details"); + } - Cell cell = container.getCell(subElement); - if (cell != null) { - // Skip the column index if subElement was a child of root - return containerType.get(i) + "[" + cell.getRow() - + (containerRow ? "]" : "][" + cell.getColumn() + "]"); - } + String editorName = getSubPartNameEditor(subElement); + if (editorName != null) { + return editorName; } - // Check if subelement is part of editor. - if (editor.getState() == State.ACTIVE) { - if (editor.editorOverlay.isOrHasChild(subElement)) { - int i = 0; - for (Column<?, T> column : columns) { - if (editor.getWidget(column).getElement() - .isOrHasChild(subElement)) { - return "editor[" + i + "]"; - } - ++i; - } - return "editor"; + return null; + } + + private String getSubPartNameEditor(Element subElement) { + + if (editor.getState() != State.ACTIVE + || !editor.editorOverlay.isOrHasChild(subElement)) { + return null; + } + + int i = 0; + for (Column<?, T> column : columns) { + if (editor.getWidget(column).getElement().isOrHasChild(subElement)) { + return "editor[" + i + "]"; } + ++i; } - return null; + return "editor"; } private void setSelectColumnRenderer( @@ -5913,6 +7055,34 @@ public class Grid<T> extends ResizeComposite implements } /** + * Register a column reorder handler to this Grid. The event for this + * handler is fired when the Grid's columns are reordered. + * + * @since 7.5.0 + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnReorderHandler( + ColumnReorderHandler<T> handler) { + return addHandler(handler, ColumnReorderEvent.getType()); + } + + /** + * Register a column visibility change handler to this Grid. The event for + * this handler is fired when the Grid's columns change visibility. + * + * @since 7.5.0 + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnVisibilityChangeHandler( + ColumnVisibilityChangeHandler<T> handler) { + return addHandler(handler, ColumnVisibilityChangeEvent.getType()); + } + + /** * Apply sorting to data source. */ private void sort(boolean userOriginated) { @@ -5964,6 +7134,27 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns whether columns can be reordered with drag and drop. + * + * @since 7.5.0 + * @return <code>true</code> if columns can be reordered, false otherwise + */ + public boolean isColumnReorderingAllowed() { + return columnReorderingAllowed; + } + + /** + * Sets whether column reordering with drag and drop is allowed or not. + * + * @since 7.5.0 + * @param columnReorderingAllowed + * specifies whether column reordering is allowed + */ + public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { + this.columnReorderingAllowed = columnReorderingAllowed; + } + + /** * Sets a new column order for the grid. All columns which are not ordered * here will remain in the order they were before as the last columns of * grid. @@ -5999,8 +7190,10 @@ public class Grid<T> extends ResizeComposite implements } columns = newOrder; + List<Column<?, T>> visibleColumns = getVisibleColumns(); + // Do ComplexRenderer.init and render new content - conf.insertColumns(0, columns.size()); + conf.insertColumns(0, visibleColumns.size()); // Number of frozen columns should be kept same #16901 updateFrozenColumns(); @@ -6017,6 +7210,10 @@ public class Grid<T> extends ResizeComposite implements for (FooterRow row : footer.getRows()) { row.calculateColspans(); } + + columnHider.updateTogglesOrder(); + + fireEvent(new ColumnReorderEvent<T>()); } /** @@ -6347,6 +7544,30 @@ public class Grid<T> extends ResizeComposite implements widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent); }-*/; + private static native final void onAttach(Widget widget) + /*-{ + widget.@Widget::onAttach()(); + }-*/; + + private static native final void onDetach(Widget widget) + /*-{ + widget.@Widget::onDetach()(); + }-*/; + + @Override + protected void doAttachChildren() { + if (getSidebar().getParent() == this) { + onAttach(getSidebar()); + } + } + + @Override + protected void doDetachChildren() { + if (getSidebar().getParent() == this) { + onDetach(getSidebar()); + } + } + /** * Resets all cached pixel sizes and reads new values from the DOM. This * methods should be used e.g. when styles affecting the dimensions of @@ -6357,6 +7578,100 @@ public class Grid<T> extends ResizeComposite implements } /** + * 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"); + } + + this.detailsGenerator = detailsGenerator; + + // this will refresh all visible spacers + escalator.getBody().setSpacerUpdater(gridSpacerUpdater); + } + + /** + * 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 row. + * <p> + * This method does nothing if trying to set show already-visible details, + * or hide already-hidden details. + * + * @since 7.5.0 + * @param rowIndex + * the index of the affected row + * @param visible + * <code>true</code> to show the details, or <code>false</code> + * to hide them + * @see #isDetailsVisible(int) + */ + public void setDetailsVisible(int rowIndex, boolean visible) { + Integer rowIndexInteger = Integer.valueOf(rowIndex); + + /* + * We want to prevent opening a details row twice, so any subsequent + * openings (or closings) of details is a NOOP. + * + * When a details row is opened, it is given an arbitrary height + * (because Escalator requires a height upon opening). Only when it's + * opened, Escalator will ask the generator to generate a widget, which + * we then can measure. When measured, we correct the initial height by + * the original height. + * + * Without this check, we would override the measured height, and revert + * back to the initial, arbitrary, height which would most probably be + * wrong. + * + * see GridSpacerUpdater.init for implementation details. + */ + + boolean isVisible = isDetailsVisible(rowIndex); + if (visible && !isVisible) { + escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT); + visibleDetails.add(rowIndexInteger); + } + + else if (!visible && isVisible) { + escalator.getBody().setSpacer(rowIndex, -1); + visibleDetails.remove(rowIndexInteger); + } + } + + /** + * Check whether the details for a row is visible or not. + * + * @since 7.5.0 + * @param rowIndex + * the index of the row for which to check details + * @return <code>true</code> iff the details for the given row is visible + * @see #setDetailsVisible(int, boolean) + */ + public boolean isDetailsVisible(int rowIndex) { + return visibleDetails.contains(Integer.valueOf(rowIndex)); + } + + /** * Requests that the column widths should be recalculated. * <p> * The actual recalculation is not necessarily done immediately so you @@ -6368,4 +7683,17 @@ public class Grid<T> extends ResizeComposite implements public void recalculateColumnWidths() { autoColumnWidthsRecalculator.schedule(); } + + /** + * Returns the sidebar for this grid. + * <p> + * The grid's sidebar shows the column hiding options for those columns that + * have been set as {@link Column#setHidable(boolean) hidable}. + * + * @since 7.5.0 + * @return the sidebar widget for this grid + */ + private Sidebar getSidebar() { + return sidebar; + } } diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 5fb0742164..e645ec60f7 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -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(); @@ -673,6 +950,14 @@ public class RpcDataProviderExtension extends AbstractExtension { 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. * * @param 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; + } } diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index a2487411c2..504e0e6eae 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -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; @@ -174,6 +176,120 @@ 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 – 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. */ @@ -343,6 +459,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 * */ @@ -2356,6 +2524,46 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } /** + * 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. * * @return the width in pixels of the column @@ -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() @@ -3887,6 +4310,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, columnOrder.addAll(stateColumnOrder); } getState().columnOrder = columnOrder; + fireColumnReorderEvent(false); } /** @@ -3918,6 +4342,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) * @@ -3929,6 +4356,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. @@ -3941,6 +4371,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. @@ -4353,6 +4786,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 @@ -4897,6 +5357,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } componentList.addAll(getEditorFields()); + + componentList.addAll(detailComponentManager.getComponents()); + return componentList.iterator(); } @@ -5414,6 +5877,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; } diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java index 1c22a69571..6cf9ef55ad 100644 --- a/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java +++ b/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java @@ -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); } diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java index b8268b1ae3..8ffe749f6f 100644 --- a/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java +++ b/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java @@ -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); diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java b/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java index 2a4b3f01fc..9424d89ecf 100644 --- a/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java +++ b/server/tests/src/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java @@ -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()); } } } diff --git a/shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java b/shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java new file mode 100644 index 0000000000..8b64d22423 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java @@ -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; + } +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java index 4ba081b5df..3c6d993482 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java @@ -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); + } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java index 4c5b2c3a02..5aa9ea9b65 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java @@ -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; } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java index c90a016383..dca55c11c4 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -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); } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java index 7018df1413..e039f70988 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -103,6 +103,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. */ public List<GridColumnState> columns = new ArrayList<GridColumnState>(); @@ -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; } diff --git a/uitest/src/com/vaadin/testbench/elements/GridElement.java b/uitest/src/com/vaadin/testbench/elements/GridElement.java deleted file mode 100644 index 68ddfc878e..0000000000 --- a/uitest/src/com/vaadin/testbench/elements/GridElement.java +++ /dev/null @@ -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)); - } -} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridScrollToRowWithDetails.java b/uitest/src/com/vaadin/tests/components/grid/GridScrollToRowWithDetails.java new file mode 100644 index 0000000000..5659f01bdd --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridScrollToRowWithDetails.java @@ -0,0 +1,133 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF 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 GridScrollToRowWithDetails 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()); + 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(false); + layout.addComponent(numberTextField); + + layout.addComponent(new Button("Toggle", new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + toggle(); + } + })); + + layout.addComponent(new Button("Scroll to", new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + scrollTo(); + } + })); + + 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; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridScrollToRowWithDetailsTest.java b/uitest/src/com/vaadin/tests/components/grid/GridScrollToRowWithDetailsTest.java new file mode 100644 index 0000000000..b6ecd3f6e2 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridScrollToRowWithDetailsTest.java @@ -0,0 +1,224 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebElement; + +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.elements.CheckBoxElement; +import com.vaadin.testbench.elements.TextFieldElement; +import com.vaadin.tests.components.grid.basicfeatures.element.CustomGridElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class GridScrollToRowWithDetailsTest extends MultiBrowserTest { + + private static class Param { + private final int rowIndex; + private final boolean useGenerator; + private final boolean scrollFirstToBottom; + private final int scrollTarget; + + public Param(int rowIndex, boolean useGenerator, + boolean scrollFirstToBottom, int scrollTarget) { + this.rowIndex = rowIndex; + this.useGenerator = useGenerator; + this.scrollFirstToBottom = scrollFirstToBottom; + this.scrollTarget = Math.max(0, scrollTarget); + } + + public int getRowIndex() { + return rowIndex; + } + + public boolean useGenerator() { + return useGenerator; + } + + public boolean scrollFirstToBottom() { + return scrollFirstToBottom; + } + + public int getScrollTarget() { + return scrollTarget; + } + + @Override + public String toString() { + return "Param [rowIndex=" + getRowIndex() + ", useGenerator=" + + useGenerator() + ", scrollFirstToBottom=" + + scrollFirstToBottom() + ", scrollTarget=" + + getScrollTarget() + "]"; + } + } + + public static Collection<Param> parameters() { + List<Param> data = new ArrayList<Param>(); + + int[][] params = new int[][] {// @formatter:off + // row, top+noGen, top+gen, bot+noGen, bot+gen + { 0, 0, 0, 0, 0 }, + { 500, 18741, 18723, 19000, 19000 }, + { 999, 37703, 37685, 37703, 37685 }, + }; + // @formatter:on + + for (int i[] : params) { + int rowIndex = i[0]; + int targetTopScrollWithoutGenerator = i[1]; + int targetTopScrollWithGenerator = i[2]; + int targetBottomScrollWithoutGenerator = i[3]; + int targetBottomScrollWithGenerator = i[4]; + + data.add(new Param(rowIndex, false, false, + targetTopScrollWithoutGenerator)); + data.add(new Param(rowIndex, true, false, + targetTopScrollWithGenerator)); + data.add(new Param(rowIndex, false, true, + targetBottomScrollWithoutGenerator)); + data.add(new Param(rowIndex, true, true, + targetBottomScrollWithGenerator)); + } + + 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()); + + Range allowedRange = Range.withLength( + param.getScrollTarget() - 5, 10); + assertTrue( + allowedRange + " does not contain " + getScrollTop(), + allowedRange.contains(getScrollTop())); + } 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()); + + Range allowedRange = Range.withLength( + param.getScrollTarget() - 5, 10); + assertTrue( + allowedRange + " does not contain " + getScrollTop(), + allowedRange.contains(getScrollTop())); + } catch (Throwable t) { + throw new Throwable("" + param, t); + } + } + } + + 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); + } + } + + @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().setValue(String.valueOf(row)); + } + + private CustomGridElement getGrid() { + return $(CustomGridElement.class).first(); + } + + private int getScrollTop() { + return ((Long) executeScript("return arguments[0].scrollTop;", + getVerticalScrollbar())).intValue(); + } + + private WebElement getVerticalScrollbar() { + WebElement scrollBar = getGrid().findElement( + By.className("v-grid-scroller-vertical")); + return scrollBar; + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index e03d50415c..862e959ebc 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -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; @@ -31,13 +33,19 @@ import com.vaadin.tests.tb3.MultiBrowserTest; @TestCategory("escalator") 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; + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java index ad3e1fe5eb..754529d10d 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java @@ -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"); + } + } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java index e5a46894b8..2abd603ae4 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java @@ -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() { @@ -248,10 +326,27 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { addFilterActions(); + addInternalActions(); + + createDetailsActions(); + this.grid = grid; return grid; } + 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 +589,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 +626,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 +756,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 +766,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 +828,37 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } }, c); + createBooleanAction("Hidable", getColumnProperty(c), false, + 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), false, + 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 +876,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 +973,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 +1257,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; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java index e22fcc422b..469bf37c2f 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java @@ -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; + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java new file mode 100644 index 0000000000..b446bdef48 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java @@ -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); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java new file mode 100644 index 0000000000..d779a5c81a --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -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")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java new file mode 100644 index 0000000000..619033226c --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java @@ -0,0 +1,242 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF 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 }); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridSidebarContentTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridSidebarContentTest.java new file mode 100644 index 0000000000..fb647c7a41 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridSidebarContentTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +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")); + + } + + private int countBySelector(String cssSelector) { + return getGridElement().findElements(By.cssSelector(cssSelector)) + .size(); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/element/CustomGridElement.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/element/CustomGridElement.java new file mode 100644 index 0000000000..e1934d4f2b --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/element/CustomGridElement.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.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)); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java index 4742236ac6..1d26477d34 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java @@ -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()); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java new file mode 100644 index 0000000000..71cc19ecdd --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -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.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(30); // 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(); + assertEquals("input", getFocusedElement().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() { + getEscalator().sendKeys( // + Keys.TAB, // v-ui v-scrollable + Keys.TAB, // menubar + Keys.TAB // <input> + ); + } + + 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)); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java new file mode 100644 index 0000000000..0d62797ea4 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java @@ -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")); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnVisibilityTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnVisibilityTest.java new file mode 100644 index 0000000000..d01e689b72 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnVisibilityTest.java @@ -0,0 +1,280 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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"); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java new file mode 100644 index 0000000000..4ea64073f3 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSidebarThemeTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSidebarThemeTest.java new file mode 100644 index 0000000000..79327993b0 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSidebarThemeTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.server; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +public class GridSidebarThemeTest extends GridBasicFeaturesTest { + + @Test + public void testValo() throws Exception { + runTestSequence("valo"); + } + + private void runTestSequence(String theme) throws IOException { + openTestURL("theme=" + theme); + if (getDesiredCapabilities().getBrowserName().equals( + Browser.CHROME.getDesiredCapabilities().getBrowserName())) { + waitUntil(ExpectedConditions.elementToBeClickable(By.id("menu")), 2); + getDriver().findElement(By.id("menu")).click(); + selectMenu("Columns"); + selectMenu("All columns hidable"); + waitUntilLoadingIndicatorNotVisible(); + } else { + selectMenuPath("Component", "Columns", "All columns hidable"); + } + + 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(); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java index f251313100..f4771b9067 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/LoadingIndicatorTest.java @@ -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(); - } } diff --git a/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java b/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java index 2d032ecd9e..48f99e5057 100644 --- a/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java +++ b/uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java @@ -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(); + } + }); + } } diff --git a/uitest/src/com/vaadin/tests/util/PersonContainer.java b/uitest/src/com/vaadin/tests/util/PersonContainer.java index 611e5d3adb..709086be29 100644 --- a/uitest/src/com/vaadin/tests/util/PersonContainer.java +++ b/uitest/src/com/vaadin/tests/util/PersonContainer.java @@ -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)); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index 761f32bc9a..c735797731 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -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); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java index 7f813b9d0f..e7ebcfeb51 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java @@ -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; } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index 4dc0195f22..196428822c 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -33,9 +33,11 @@ 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.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 +48,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 +58,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 +80,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 +408,7 @@ public class GridBasicClientFeaturesWidget extends createEditorMenu(); createInternalsMenu(); createDataSourceMenu(); + createDetailsMenu(); grid.getElement().getStyle().setZIndex(0); @@ -444,6 +453,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 +732,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 +818,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 +926,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 +1400,82 @@ 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); + } + + } } diff --git a/widgets/build.xml b/widgets/build.xml index f8aba7fc81..52888cc9c8 100644 --- a/widgets/build.xml +++ b/widgets/build.xml @@ -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" /> |