diff options
173 files changed, 31603 insertions, 25 deletions
diff --git a/WebContent/VAADIN/themes/base/base.scss b/WebContent/VAADIN/themes/base/base.scss index 3570c5efec..6263646ce1 100644 --- a/WebContent/VAADIN/themes/base/base.scss +++ b/WebContent/VAADIN/themes/base/base.scss @@ -16,8 +16,10 @@ @import "inlinedatefield/inlinedatefield.scss"; @import "dragwrapper/dragwrapper.scss"; @import "embedded/embedded.scss"; +@import "escalator/escalator.scss"; @import "fonts/fonts.scss"; @import "formlayout/formlayout.scss"; +@import "grid/grid.scss"; @import "gridlayout/gridlayout.scss"; @import "label/label.scss"; @import "link/link.scss"; @@ -90,7 +92,9 @@ $line-height: normal; @include base-inline-datefield; @include base-dragwrapper; @include base-embedded; + @include base-escalator; @include base-formlayout; + @include base-grid; @include base-gridlayout; @include base-label; @include base-link; diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss new file mode 100644 index 0000000000..0246224fd3 --- /dev/null +++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss @@ -0,0 +1,120 @@ +@mixin base-escalator($primaryStyleName : v-escalator) { + +$background-color: white; +$border-color: #aaa; + +.#{$primaryStyleName} { + position: relative; + background-color: $background-color; +} + +.#{$primaryStyleName}-scroller { + position: absolute; + overflow: auto; + z-index: 20; +} + +.#{$primaryStyleName}-scroller-horizontal { + left: 0; /* Left position adjusted to align with frozen columns */ + right: 0; + bottom: 0; + overflow-y: hidden; + -ms-overflow-y: hidden; +} + +.#{$primaryStyleName}-scroller-vertical { + right: 0; + top: 0; /* this will be overridden by code, but it's a good default behavior */ + bottom: 0; /* this will be overridden by code, but it's a good default behavior */ + overflow-x: hidden; + -ms-overflow-x: hidden; +} + +.#{$primaryStyleName}-tablewrapper { + position: absolute; + overflow: hidden; +} + +.#{$primaryStyleName}-tablewrapper > table { + border-spacing: 0; + table-layout: fixed; + width: inherit; /* a decent default fallback */ +} + +.#{$primaryStyleName}-header, +.#{$primaryStyleName}-body, +.#{$primaryStyleName}-footer { + position: absolute; + left: 0; + width: inherit; + z-index: 10; +} + +.#{$primaryStyleName}-header { top: 0; } +.#{$primaryStyleName}-footer { bottom: 0; } + +.#{$primaryStyleName}-body { + z-index: 0; + top: 0; + + .#{$primaryStyleName}-row { + position: absolute; + top: 0; + left: 0; + } +} + +.#{$primaryStyleName}-row { + display: block; + + .v-ie8 &, .v-ie9 & { + /* + * Neither IE8 nor IE9 let table rows be longer than tbody, with only + * "display: block". Moar hax. + */ + float: left; + clear: left; + + /* + * The inline style of margin-top from the <tbody> to offset the + * header's dimension is, for some strange reason, inherited into each + * contained <tr>. We need to cancel it: + */ + margin-top: 0; + } + + > td, > th { + /* IE8 likes the bgcolor here instead of on the row */ + background-color: $background-color; + } +} + + +.#{$primaryStyleName}-row { + width: inherit; +} + +.#{$primaryStyleName}-cell { + display: block; + float: left; + border: 1px solid $border-color; + padding: 2px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow:hidden; + + /* + * Because Vaadin changes the font size after the initial render, we + * need to mention the font size here explicitly, otherwise automatic + * row height detection gets broken. + */ + font-size: $font-size; +} + +.#{$primaryStyleName}-cell.frozen { + position: relative; + z-index: 1; +} + +} diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss new file mode 100644 index 0000000000..88c7754a10 --- /dev/null +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -0,0 +1,38 @@ +@mixin base-grid($primaryStyleName : v-grid) { + @include base-escalator($primaryStyleName); + + .#{$primaryStyleName} { + + th { + position: relative; + } + + th.sort-asc:after { + content: "\25B2" attr(sort-order); + position: absolute; + right: 5px; + } + + th.sort-desc:after { + content: "\25BC" attr(sort-order); + position: absolute; + right: 5px; + } + + .#{$primaryStyleName}-cell-active { + border-color: blue; + } + + .#{$primaryStyleName}-header-active { + background: lightgray; + } + + .#{$primaryStyleName}-row-active > td { + background: rgb(244,244,244); + } + } + + .#{$primaryStyleName}-row-selected > td { + background: lightblue; + } +} diff --git a/WebContent/VAADIN/themes/reindeer-tests/styles.css b/WebContent/VAADIN/themes/reindeer-tests/styles.css index 679de01b9c..9dd88707d1 100644 --- a/WebContent/VAADIN/themes/reindeer-tests/styles.css +++ b/WebContent/VAADIN/themes/reindeer-tests/styles.css @@ -32,3 +32,7 @@ .popup-style .v-datefield-calendarpanel-body { background: yellow; } + +#escalator .v-escalator-body .v-escalator-cell { + height: 50px; +}
\ No newline at end of file diff --git a/WebContent/release-notes.html b/WebContent/release-notes.html index 0ede61d729..3fa008f3f3 100644 --- a/WebContent/release-notes.html +++ b/WebContent/release-notes.html @@ -97,11 +97,7 @@ enhancements. Below is a list of the most notable changes:</p> <ul> - <li>Valo is a brand new built-in theme for Vaadin. It leverages - the Sass CSS preprocessor heavily, - providing a variety of ways to customize the look and feel of your theme. - See <a href="https://vaadin.com/wiki/-/wiki/Main/Valo+theme+-+Getting+started">the Valo theme tutorial</a> or <a href="https://vaadin.com/book/-/page/themes.valo.html">the Valo theme section</a> in Book of Vaadin for information on how to get started.</li> - <li>Support for changing theme on the fly</li> + <li>Grid</li> </ul> <p> diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java index a6ca690a8a..7c3bb1eb77 100644 --- a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java +++ b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java @@ -61,6 +61,7 @@ import com.vaadin.server.widgetsetutils.metadata.ConnectorInitVisitor; import com.vaadin.server.widgetsetutils.metadata.GeneratedSerializer; import com.vaadin.server.widgetsetutils.metadata.OnStateChangeVisitor; import com.vaadin.server.widgetsetutils.metadata.Property; +import com.vaadin.server.widgetsetutils.metadata.RendererVisitor; import com.vaadin.server.widgetsetutils.metadata.ServerRpcVisitor; import com.vaadin.server.widgetsetutils.metadata.StateInitVisitor; import com.vaadin.server.widgetsetutils.metadata.TypeVisitor; @@ -503,6 +504,7 @@ public class ConnectorBundleLoaderFactory extends Generator { // this after the JS property data has been initialized writePropertyTypes(logger, w, bundle); writeSerializers(logger, w, bundle); + writePresentationTypes(w, bundle); writeDelegateToWidget(logger, w, bundle); writeOnStateChangeHandlers(logger, w, bundle); } @@ -684,6 +686,21 @@ public class ConnectorBundleLoaderFactory extends Generator { } } + private void writePresentationTypes(SplittingSourceWriter w, + ConnectorBundle bundle) { + Map<JClassType, JType> presentationTypes = bundle + .getPresentationTypes(); + for (Entry<JClassType, JType> entry : presentationTypes.entrySet()) { + + w.print("store.setPresentationType("); + writeClassLiteral(w, entry.getKey()); + w.print(", "); + writeClassLiteral(w, entry.getValue()); + w.println(");"); + w.splitIfNeeded(); + } + } + private void writePropertyTypes(TreeLogger logger, SplittingSourceWriter w, ConnectorBundle bundle) { Set<Property> properties = bundle.getNeedsProperty(); @@ -1240,8 +1257,9 @@ public class ConnectorBundleLoaderFactory extends Generator { throws NotFoundException { List<TypeVisitor> visitors = Arrays.<TypeVisitor> asList( new ConnectorInitVisitor(), new StateInitVisitor(), - new WidgetInitVisitor(), new ClientRpcVisitor(), - new ServerRpcVisitor(), new OnStateChangeVisitor()); + new WidgetInitVisitor(), new RendererVisitor(), + new ClientRpcVisitor(), new ServerRpcVisitor(), + new OnStateChangeVisitor()); for (TypeVisitor typeVisitor : visitors) { typeVisitor.init(oracle); } diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java index e8a384298f..4a079c38b0 100644 --- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java +++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java @@ -44,6 +44,7 @@ import com.vaadin.client.ComponentConnector; import com.vaadin.client.ServerConnector; import com.vaadin.client.communication.JSONSerializer; import com.vaadin.client.ui.UnknownComponentConnector; +import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector; import com.vaadin.shared.communication.ClientRpc; import com.vaadin.shared.communication.ServerRpc; import com.vaadin.shared.ui.Connect; @@ -59,6 +60,7 @@ public class ConnectorBundle { private final Set<JType> hasSerializeSupport = new HashSet<JType>(); private final Set<JType> needsSerializeSupport = new HashSet<JType>(); private final Map<JType, GeneratedSerializer> serializers = new HashMap<JType, GeneratedSerializer>(); + private final Map<JClassType, JType> presentationTypes = new HashMap<JClassType, JType>(); private final Set<JClassType> needsSuperClass = new HashSet<JClassType>(); private final Set<JClassType> needsGwtConstructor = new HashSet<JClassType>(); @@ -306,6 +308,25 @@ public class ConnectorBundle { return Collections.unmodifiableMap(serializers); } + public void setPresentationType(JClassType type, JType presentationType) { + if (!hasPresentationType(type)) { + presentationTypes.put(type, presentationType); + } + } + + private boolean hasPresentationType(JClassType type) { + if (presentationTypes.containsKey(type)) { + return true; + } else { + return previousBundle != null + && previousBundle.hasPresentationType(type); + } + } + + public Map<JClassType, JType> getPresentationTypes() { + return Collections.unmodifiableMap(presentationTypes); + } + private void setNeedsSuperclass(JClassType typeAsClass) { if (!isNeedsSuperClass(typeAsClass)) { needsSuperClass.add(typeAsClass); @@ -415,6 +436,11 @@ public class ConnectorBundle { return isConnected(type) && isType(type, ComponentConnector.class); } + public static boolean isConnectedRendererConnector(JClassType type) { + return isConnected(type) + && isType(type, AbstractRendererConnector.class); + } + private static boolean isInterfaceType(JClassType type, Class<?> class1) { return type.isInterface() != null && isType(type, class1); } diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java new file mode 100644 index 0000000000..6c6d6d116c --- /dev/null +++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.server.widgetsetutils.metadata; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JType; +import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector; + +/** + * Generates type data for renderer connectors. + * <ul> + * <li>Stores the return type of the overridden + * {@link AbstractRendererConnector#getRenderer() getRenderer} method to enable + * automatic creation of an instance of the proper renderer type. + * <li>Stores the presentation type of the connector to enable the + * {@link AbstractRendererConnector#decode(com.google.gwt.json.client.JSONValue) + * decode} method to work without having to implement a "getPresentationType" + * method. + * </ul> + * + * @see WidgetInitVisitor + * + * @since + * @author Vaadin Ltd + */ +public class RendererVisitor extends TypeVisitor { + + @Override + public void visitConnector(TreeLogger logger, JClassType type, + ConnectorBundle bundle) { + if (ConnectorBundle.isConnectedRendererConnector(type)) { + doRendererType(logger, type, bundle); + doPresentationType(logger, type, bundle); + } + } + + private static void doRendererType(TreeLogger logger, JClassType type, + ConnectorBundle bundle) { + // The class in which createRenderer is implemented + JClassType createRendererClass = ConnectorBundle.findInheritedMethod( + type, "createRenderer").getEnclosingType(); + + // Needs GWT constructor if createRenderer is not overridden + if (createRendererClass.getQualifiedSourceName().equals( + AbstractRendererConnector.class.getCanonicalName())) { + + JMethod getRenderer = ConnectorBundle.findInheritedMethod(type, + "getRenderer"); + JClassType rendererType = getRenderer.getReturnType().isClass(); + + bundle.setNeedsGwtConstructor(rendererType); + + // Also needs renderer type to find the right GWT constructor + bundle.setNeedsReturnType(type, getRenderer); + + logger.log(Type.DEBUG, "Renderer type of " + type + " is " + + rendererType); + } + } + + private void doPresentationType(TreeLogger logger, JClassType type, + ConnectorBundle bundle) { + JType presentationType = getPresentationType(type); + bundle.setPresentationType(type, presentationType); + + logger.log(Type.DEBUG, "Presentation type of " + type + " is " + + presentationType); + } + + private static JType getPresentationType(JClassType type) { + JClassType originalType = type; + while (type != null) { + if (type.getQualifiedBinaryName().equals( + AbstractRendererConnector.class.getName())) { + return type.isParameterized().getTypeArgs()[0]; + } + type = type.getSuperclass(); + } + throw new IllegalArgumentException("The type " + + originalType.getQualifiedSourceName() + " does not extend " + + AbstractRendererConnector.class.getName()); + } +} diff --git a/client/ivy.xml b/client/ivy.xml index 3abdcf9ba5..6b941af818 100644 --- a/client/ivy.xml +++ b/client/ivy.xml @@ -42,6 +42,9 @@ <dependency org="javax.validation" name="validation-api" rev="1.0.0.GA" conf="build->default,sources" /> + <!-- Testing dependencies --> + <dependency org="org.easymock" name="easymock" rev="3.0" + conf="test,ide-> default" /> </dependencies> diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java new file mode 100644 index 0000000000..ca2d2bc83e --- /dev/null +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -0,0 +1,570 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.data; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.vaadin.client.Profiler; +import com.vaadin.shared.ui.grid.Range; + +/** + * Base implementation for data sources that fetch data from a remote system. + * This class takes care of caching data and communicating with the data source + * user. An implementation of this class should override + * {@link #requestRows(int, int)} to trigger asynchronously loading of data. + * When data is received from the server, new row data should be passed to + * {@link #setRowData(int, List)}. {@link #setEstimatedSize(int)} should be used + * based on estimations of how many rows are available. + * + * @since + * @author Vaadin Ltd + * @param <T> + * the row type + */ +public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { + + protected class RowHandleImpl extends RowHandle<T> { + private T row; + private final Object key; + + public RowHandleImpl(final T row, final Object key) { + this.row = row; + this.key = key; + } + + /** + * A method for the data source to update the row data. + * + * @param row + * the updated row object + */ + public void setRow(final T row) { + this.row = row; + assert getRowKey(row).equals(key) : "The old key does not " + + "equal the new key for the given row (old: " + key + + ", new :" + getRowKey(row) + ")"; + } + + @Override + public T getRow() throws IllegalStateException { + if (isPinned()) { + return row; + } else { + throw new IllegalStateException("The row handle for key " + key + + " was not pinned"); + } + } + + private boolean isPinned() { + return pinnedRows.containsKey(key); + } + + @Override + public void pin() { + Integer count = pinnedCounts.get(key); + if (count == null) { + count = Integer.valueOf(0); + pinnedRows.put(key, this); + } + pinnedCounts.put(key, Integer.valueOf(count.intValue() + 1)); + } + + @Override + public void unpin() throws IllegalStateException { + final Integer count = pinnedCounts.get(key); + if (count == null) { + throw new IllegalStateException("Row " + row + " with key " + + key + " was not pinned to begin with"); + } else if (count.equals(Integer.valueOf(1))) { + pinnedRows.remove(key); + pinnedCounts.remove(key); + } else { + pinnedCounts.put(key, Integer.valueOf(count.intValue() - 1)); + } + } + + @Override + protected boolean equalsExplicit(final Object obj) { + if (obj instanceof AbstractRemoteDataSource.RowHandleImpl) { + /* + * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I + * like the @SuppressWarnings more (keeps the line length in + * check.) + */ + @SuppressWarnings("unchecked") + final RowHandleImpl rhi = (RowHandleImpl) obj; + return key.equals(rhi.key); + } else { + return false; + } + } + + @Override + protected int hashCodeExplicit() { + return key.hashCode(); + } + } + + /** + * Records the start of the previously requested range. This is used when + * tracking request timings to distinguish between explicit responses and + * arbitrary updates pushed from the server. + */ + private int lastRequestStart = -1; + private double pendingRequestTime; + + private boolean coverageCheckPending = false; + + private Range requestedAvailability = Range.between(0, 0); + + private Range cached = Range.between(0, 0); + + private final HashMap<Integer, T> rowCache = new HashMap<Integer, T>(); + + private DataChangeHandler dataChangeHandler; + + private Range estimatedAvailableRange = Range.between(0, 0); + + private CacheStrategy cacheStrategy = new CacheStrategy.DefaultCacheStrategy(); + + private final ScheduledCommand coverageChecker = new ScheduledCommand() { + @Override + public void execute() { + coverageCheckPending = false; + checkCacheCoverage(); + } + }; + + private Map<Object, Integer> pinnedCounts = new HashMap<Object, Integer>(); + private Map<Object, RowHandleImpl> pinnedRows = new HashMap<Object, RowHandleImpl>(); + protected Collection<T> temporarilyPinnedRows = Collections.emptySet(); + + /** + * Sets the estimated number of rows in the data source. + * + * @param estimatedSize + * the estimated number of available rows + */ + protected void setEstimatedSize(int estimatedSize) { + // TODO update dataChangeHandler if size changes + estimatedAvailableRange = Range.withLength(0, estimatedSize); + } + + private void ensureCoverageCheck() { + if (!coverageCheckPending) { + coverageCheckPending = true; + Scheduler.get().scheduleDeferred(coverageChecker); + } + } + + @Override + public void ensureAvailability(int firstRowIndex, int numberOfRows) { + requestedAvailability = Range.withLength(firstRowIndex, numberOfRows); + + /* + * Don't request any data right away since the data might be included in + * a message that has been received but not yet fully processed. + */ + ensureCoverageCheck(); + } + + private void checkCacheCoverage() { + if (lastRequestStart != -1) { + // Anyone clearing lastRequestStart should run this method again + return; + } + + Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage"); + + Range minCacheRange = getMinCacheRange(); + + if (!minCacheRange.intersects(cached) || cached.isEmpty()) { + /* + * Simple case: no overlap between cached data and needed data. + * Clear the cache and request new data + */ + rowCache.clear(); + cached = Range.between(0, 0); + + handleMissingRows(getMaxCacheRange()); + } else { + discardStaleCacheEntries(); + + // Might need more rows -> request them + if (!minCacheRange.isSubsetOf(cached)) { + Range[] missingCachePartition = getMaxCacheRange() + .partitionWith(cached); + handleMissingRows(missingCachePartition[0]); + handleMissingRows(missingCachePartition[2]); + } + } + + Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage"); + } + + private void discardStaleCacheEntries() { + Range[] cacheParition = cached.partitionWith(getMaxCacheRange()); + dropFromCache(cacheParition[0]); + cached = cacheParition[1]; + dropFromCache(cacheParition[2]); + } + + private void dropFromCache(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + private void handleMissingRows(Range range) { + if (range.isEmpty()) { + return; + } + lastRequestStart = range.getStart(); + pendingRequestTime = Duration.currentTimeMillis(); + requestRows(range.getStart(), range.length()); + } + + /** + * Triggers fetching rows from the remote data source. + * {@link #setRowData(int, List)} should be invoked with data for the + * requested rows when they have been received. + * + * @param firstRowIndex + * the index of the first row to fetch + * @param numberOfRows + * the number of rows to fetch + */ + protected abstract void requestRows(int firstRowIndex, int numberOfRows); + + @Override + public int getEstimatedSize() { + return estimatedAvailableRange.length(); + } + + @Override + public T getRow(int rowIndex) { + return rowCache.get(Integer.valueOf(rowIndex)); + } + + @Override + public void setDataChangeHandler(DataChangeHandler dataChangeHandler) { + this.dataChangeHandler = dataChangeHandler; + + if (dataChangeHandler != null && !cached.isEmpty()) { + // Push currently cached data to the implementation + dataChangeHandler.dataUpdated(cached.getStart(), cached.length()); + dataChangeHandler.dataAvailable(cached.getStart(), cached.length()); + } + } + + /** + * Informs this data source that updated data has been sent from the server. + * + * @param firstRowIndex + * the index of the first received row + * @param rowData + * a list of rows, starting from <code>firstRowIndex</code> + */ + protected void setRowData(int firstRowIndex, List<T> rowData) { + + Profiler.enter("AbstractRemoteDataSource.setRowData"); + + Range received = Range.withLength(firstRowIndex, rowData.size()); + + if (firstRowIndex == lastRequestStart) { + // Provide timing information if we know when we asked for this data + cacheStrategy.onDataArrive(Duration.currentTimeMillis() + - pendingRequestTime, received.length()); + } + lastRequestStart = -1; + + Range maxCacheRange = getMaxCacheRange(); + + Range[] partition = received.partitionWith(maxCacheRange); + + Range newUsefulData = partition[1]; + if (!newUsefulData.isEmpty()) { + // Update the parts that are actually inside + for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) { + rowCache.put(Integer.valueOf(i), rowData.get(i - firstRowIndex)); + } + + if (dataChangeHandler != null) { + Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler"); + dataChangeHandler.dataUpdated(newUsefulData.getStart(), + newUsefulData.length()); + Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler"); + } + + // Potentially extend the range + if (cached.isEmpty()) { + cached = newUsefulData; + } else { + discardStaleCacheEntries(); + + /* + * everything might've become stale so we need to re-check for + * emptiness. + */ + if (!cached.isEmpty()) { + cached = cached.combineWith(newUsefulData); + } else { + cached = newUsefulData; + } + } + dataChangeHandler.dataAvailable(cached.getStart(), cached.length()); + + updatePinnedRows(rowData); + } + + if (!partition[0].isEmpty() || !partition[2].isEmpty()) { + /* + * FIXME + * + * Got data that we might need in a moment if the container is + * updated before the widget settings. Support for this will be + * implemented later on. + */ + } + + // Eventually check whether all needed rows are now available + ensureCoverageCheck(); + + Profiler.leave("AbstractRemoteDataSource.setRowData"); + } + + private void updatePinnedRows(final List<T> rowData) { + for (final T row : rowData) { + final Object key = getRowKey(row); + final RowHandleImpl handle = pinnedRows.get(key); + if (handle != null) { + handle.setRow(row); + } + } + } + + /** + * Informs this data source that the server has removed data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of removed rows, starting from + * <code>firstRowIndex</code> + */ + protected void removeRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.removeRowData"); + + // pack the cached data + for (int i = 0; i < count; i++) { + Integer oldIndex = Integer.valueOf(firstRowIndex + count + i); + if (rowCache.containsKey(oldIndex)) { + Integer newIndex = Integer.valueOf(firstRowIndex + i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + Range removedRange = Range.withLength(firstRowIndex, count); + if (cached.isSubsetOf(removedRange)) { + cached = Range.withLength(0, 0); + } else if (removedRange.intersects(cached)) { + Range[] partitions = cached.partitionWith(removedRange); + Range remainsBefore = partitions[0]; + Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange + .length()); + cached = remainsBefore.combineWith(transposedRemainsAfter); + } + setEstimatedSize(getEstimatedSize() - count); + dataChangeHandler.dataRemoved(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.removeRowData"); + } + + /** + * Informs this data source that new data has been inserted from the server. + * + * @param firstRowIndex + * the destination index of the new row data + * @param count + * the number of rows inserted + */ + protected void insertRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.insertRowData"); + + if (cached.contains(firstRowIndex)) { + int oldCacheEnd = cached.getEnd(); + /* + * We need to invalidate the cache from the inserted row onwards, + * since the cache wants to be a contiguous range. It doesn't + * support holes. + * + * If holes were supported, we could shift the higher part of + * "cached" and leave a hole the size of "count" in the middle. + */ + cached = cached.splitAt(firstRowIndex)[0]; + + for (int i = firstRowIndex; i < oldCacheEnd; i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + else if (firstRowIndex < cached.getStart()) { + Range oldCached = cached; + cached = cached.offsetBy(count); + + for (int i = 0; i < rowCache.size(); i++) { + Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i); + Integer newIndex = Integer.valueOf(cached.getEnd() - i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + setEstimatedSize(getEstimatedSize() + count); + dataChangeHandler.dataAdded(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.insertRowData"); + } + + /** + * Gets the current range of cached rows + * + * @return the range of currently cached rows + */ + public Range getCachedRange() { + return cached; + } + + /** + * Sets the cache strategy that is used to determine how much data is + * fetched and cached. + * <p> + * The new strategy is immediately used to evaluate whether currently cached + * rows should be discarded or new rows should be fetched. + * + * @param cacheStrategy + * a cache strategy implementation, not <code>null</code> + */ + public void setCacheStrategy(CacheStrategy cacheStrategy) { + if (cacheStrategy == null) { + throw new IllegalArgumentException(); + } + + if (this.cacheStrategy != cacheStrategy) { + this.cacheStrategy = cacheStrategy; + + checkCacheCoverage(); + } + } + + private Range getMinCacheRange() { + Range minCacheRange = cacheStrategy.getMinCacheRange( + requestedAvailability, cached, estimatedAvailableRange); + + assert minCacheRange.isSubsetOf(estimatedAvailableRange); + + return minCacheRange; + } + + private Range getMaxCacheRange() { + Range maxCacheRange = cacheStrategy.getMaxCacheRange( + requestedAvailability, cached, estimatedAvailableRange); + + assert maxCacheRange.isSubsetOf(estimatedAvailableRange); + + return maxCacheRange; + } + + @Override + public RowHandle<T> getHandle(T row) throws IllegalStateException { + Object key = getRowKey(row); + + if (key == null) { + throw new NullPointerException("key may not be null (row: " + row + + ")"); + } + + if (pinnedRows.containsKey(key)) { + return pinnedRows.get(key); + } else if (rowCache.containsValue(row)) { + return new RowHandleImpl(row, key); + } else { + throw new IllegalStateException("The cache of this DataSource " + + "does not currently contain the row " + row); + } + } + + /** + * Gets a stable key for the row object. + * <p> + * This method is a workaround for the fact that there is no means to force + * proper implementations for {@link #hashCode()} and + * {@link #equals(Object)} methods. + * <p> + * Since the same row object will be created several times for the same + * logical data, the DataSource needs a mechanism to be able to compare two + * objects, and figure out whether or not they represent the same data. Even + * if all the fields of an entity would be changed, it still could represent + * the very same thing (say, a person changes all of her names.) + * <p> + * A very usual and simple example what this could be, is an unique ID for + * this object that would also be stored in a database. + * + * @param row + * the row object for which to get the key + * @return a non-null object that uniquely and consistently represents the + * row object + */ + abstract public Object getRowKey(T row); + + /** + * Marks rows as pinned when fetching new rows. + * <p> + * This collection of rows are intended to remain pinned if new rows are + * fetched from the data source, even if some of the pinned rows would fall + * off the cache and become inactive. + * <p> + * This method does nothing by itself, other than it stores the rows into a + * field. The implementation needs to make all the adjustments for itself. + * Check {@link RpcDataSourceConnector.RpcDataSource#requestRows(int, int)} + * for an implementation example. + * + * @param keys + * a collection of rows to keep pinned + * + * @see #temporarilyPinnedRows + * @see RpcDataSourceConnector.RpcDataSource#requestRows(int, int) + * @deprecated You probably don't want to call this method unless you're + * writing a Renderer for a selection model. Even if you are, be + * very aware what this method does and how it behaves. + */ + @Deprecated + public void transactionPin(Collection<T> rows) { + if (rows == null) { + throw new IllegalArgumentException("argument may not be null"); + } + temporarilyPinnedRows = rows; + } +} diff --git a/client/src/com/vaadin/client/data/CacheStrategy.java b/client/src/com/vaadin/client/data/CacheStrategy.java new file mode 100644 index 0000000000..3448659e61 --- /dev/null +++ b/client/src/com/vaadin/client/data/CacheStrategy.java @@ -0,0 +1,183 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.data; + +import com.vaadin.shared.ui.grid.Range; + +/** + * Determines what data an {@link AbstractRemoteDataSource} should fetch and + * keep cached. + * + * @since + * @author Vaadin Ltd + */ +public interface CacheStrategy { + /** + * A helper class for creating a simple symmetric cache strategy that uses + * the same logic for both rows before and after the currently cached range. + * <p> + * This simple approach rules out more advanced heuristics that would take + * the current scrolling direction or past scrolling behavior into account. + */ + public static abstract class AbstractBasicSymmetricalCacheStrategy + implements CacheStrategy { + + @Override + public void onDataArrive(double roundTripTime, int rowCount) { + // NOP + } + + @Override + public Range getMinCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange) { + int cacheSize = getMinimumCacheSize(displayedRange.length()); + + return displayedRange.expand(cacheSize, cacheSize).restrictTo( + estimatedAvailableRange); + } + + @Override + public Range getMaxCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange) { + int cacheSize = getMaximumCacheSize(displayedRange.length()); + + return displayedRange.expand(cacheSize, cacheSize).restrictTo( + estimatedAvailableRange); + } + + /** + * Gets the maximum number of extra items to cache in one direction. + * + * @param pageSize + * the current number of items used at once + * @return maximum of items to cache + */ + public abstract int getMaximumCacheSize(int pageSize); + + /** + * Gets the the minimum number of extra items to cache in one direction. + * + * @param pageSize + * the current number of items used at once + * @return minimum number of items to cache + */ + public abstract int getMinimumCacheSize(int pageSize); + } + + /** + * The default cache strategy used by {@link AbstractRemoteDataSource}, + * using multiples of the page size for determining the minimum and maximum + * number of items to keep in the cache. By default, at least three times + * the page size both before and after the currently used range are kept in + * the cache and items are discarded if there's yet another page size worth + * of items cached in either direction. + */ + public static class DefaultCacheStrategy extends + AbstractBasicSymmetricalCacheStrategy { + private final int minimumRatio; + private final int maximumRatio; + + /** + * Creates a DefaultCacheStrategy keeping between 3 and 4 pages worth of + * data cached both before and after the active range. + */ + public DefaultCacheStrategy() { + this(3, 4); + } + + /** + * Creates a DefaultCacheStrategy with custom ratios for how much data + * to cache. The ratios denote how many multiples of the currently used + * page size are kept in the cache in each direction. + * + * @param minimumRatio + * the minimum number of pages to keep in the cache in each + * direction + * @param maximumRatio + * the maximum number of pages to keep in the cache in each + * direction + */ + public DefaultCacheStrategy(int minimumRatio, int maximumRatio) { + this.minimumRatio = minimumRatio; + this.maximumRatio = maximumRatio; + } + + @Override + public int getMinimumCacheSize(int pageSize) { + return pageSize * minimumRatio; + } + + @Override + public int getMaximumCacheSize(int pageSize) { + return pageSize * maximumRatio; + } + } + + /** + * Called whenever data requested by the data source has arrived. This + * information can e.g. be used for measuring how long it takes to fetch + * different number of rows from the server. + * <p> + * A cache strategy implementation cannot use this information to keep track + * of which items are in the cache since the data source might discard items + * without notifying the cache strategy. + * + * @param roundTripTime + * the total number of milliseconds elapsed from requesting the + * data until the response was passed to the data source + * @param rowCount + * the number of received rows + */ + public void onDataArrive(double roundTripTime, int rowCount); + + /** + * Gets the minimum row range that should be cached. The data source will + * fetch new data if the currently cached range does not fill the entire + * minimum cache range. + * + * @param displayedRange + * the range of currently displayed rows + * @param cachedRange + * the range of currently cached rows + * @param estimatedAvailableRange + * the estimated range of rows available for the data source + * + * @return the minimum range of rows that should be cached, should at least + * include the displayed range and should not exceed the total + * estimated available range + */ + public Range getMinCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange); + + /** + * Gets the maximum row range that should be cached. The data source will + * discard cached rows that are outside the maximum range. + * + * @param displayedRange + * the range of currently displayed rows + * @param cachedRange + * the range of currently cached rows + * @param estimatedAvailableRange + * the estimated range of rows available for the data source + * + * @return the maximum range of rows that should be cached, should at least + * include the displayed range and should not exceed the total + * estimated available range + */ + public Range getMaxCacheRange(Range displayedRange, Range cachedRange, + Range estimatedAvailableRange); +} diff --git a/client/src/com/vaadin/client/data/DataChangeHandler.java b/client/src/com/vaadin/client/data/DataChangeHandler.java new file mode 100644 index 0000000000..fe72fe673a --- /dev/null +++ b/client/src/com/vaadin/client/data/DataChangeHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.data; + +/** + * Callback interface used by {@link DataSource} to inform its user about + * updates to the data. + * + * @since + * @author Vaadin Ltd + */ +public interface DataChangeHandler { + /** + * Called when the contents of the data source has changed. If the number of + * rows has changed or if rows have been moved around, + * {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should + * ideally be used instead. + * + * @param firstRowIndex + * the index of the first changed row + * @param numberOfRows + * the number of changed rows + */ + public void dataUpdated(int firstRowIndex, int numberOfRows); + + /** + * Called when rows have been removed from the data source. + * + * @param firstRowIndex + * the index that the first removed row had prior to removal + * @param numberOfRows + * the number of removed rows + */ + public void dataRemoved(int firstRowIndex, int numberOfRows); + + /** + * Called when the new rows have been added to the container. + * + * @param firstRowIndex + * the index of the first added row + * @param numberOfRows + * the number of added rows + */ + public void dataAdded(int firstRowIndex, int numberOfRows); + + /** + * Called when rows requested with + * {@link DataSource#ensureAvailability(int, int)} rows are available. + * + * @param firstRowIndex + * the index of the first available row + * @param numberOfRows + * the number of available rows + */ + public void dataAvailable(int firstRowIndex, int numberOfRows); +} diff --git a/client/src/com/vaadin/client/data/DataSource.java b/client/src/com/vaadin/client/data/DataSource.java new file mode 100644 index 0000000000..24cef548c5 --- /dev/null +++ b/client/src/com/vaadin/client/data/DataSource.java @@ -0,0 +1,188 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.data; + +/** + * Source of data for widgets showing lazily loaded data based on indexable + * items (e.g. rows) of a specified type. The data source is a lazy view into a + * larger data set. + * + * @since + * @author Vaadin Ltd + * @param <T> + * the row type + */ +public interface DataSource<T> { + + /** + * A handle that contains information on whether a row should be + * {@link #pin() pinned} or {@link #unpin() unpinned}, and also always the + * most recent representation for that particular row. + * + * @param <T> + * the row type + */ + public abstract class RowHandle<T> { + /** + * Gets the most recent representation for the row this handle + * represents. + * + * @return the most recent representation for the row this handle + * represents + * @throws IllegalStateException + * if this row handle isn't currently pinned + * @see #pin() + */ + public abstract T getRow() throws IllegalStateException; + + /** + * Marks this row as pinned. + * <p> + * <em>Note:</em> Pinning a row multiple times requires an equal amount + * of unpins to free the row from the "pinned" status. + * <p> + * <em>Technical Note:</em> Pinning a row makes sure that the row object + * for a particular set of data is always kept as up to date as the data + * source is able to. Since the DataSource might create a new instance + * of an object, object references aren't necessarily kept up-to-date. + * This is a technical work-around for that. + * + * @see #unpin() + */ + public abstract void pin(); + + /** + * Marks this row as unpinned. + * <p> + * <em>Note:</em> Pinning a row multiple times requires an equal amount + * of unpins to free the row from the "pinned" status. + * <p> + * <em>Technical Note:</em> Pinning a row makes sure that the row object + * for a particular set of data is always kept as up to date as the data + * source is able to. Since the DataSource might create a new instance + * of an object, object references aren't necessarily kept up-to-date. + * This is a technical work-around for that. + * + * @throws IllegalStateException + * if this row handle has not been pinned before + * @see #pin() + */ + public abstract void unpin() throws IllegalStateException; + + /** + * An explicit override for {@link Object#equals(Object)}. This method + * should be functionally equivalent to a properly implemented equals + * method. + * <p> + * Having a properly implemented equals method is imperative for + * RowHandle to function. Because Java has no mechanism to force an + * override of an existing method, we're defining a new method for that + * instead. + * + * @param rowHandle + * the reference object with which to compare + * @return {@code true} if this object is the same as the obj argument; + * {@code false} otherwise. + */ + protected abstract boolean equalsExplicit(Object obj); + + /** + * An explicit override for {@link Object#hashCode()}. This method + * should be functionally equivalent to a properly implemented hashCode + * method. + * <p> + * Having a properly implemented hashCode method is imperative for + * RowHandle to function. Because Java has no mechanism to force an + * override of an existing method, we're defining a new method for that + * instead. + * + * @return a hash code value for this object + */ + protected abstract int hashCodeExplicit(); + + @Override + public int hashCode() { + return hashCodeExplicit(); + } + + @Override + public boolean equals(Object obj) { + return equalsExplicit(obj); + } + } + + /** + * Informs the data source that data for the given range is needed. A data + * source only has one active region at a time, so calling this method + * discards the previously set range. + * <p> + * This method triggers lazy loading of data if necessary. The change + * handler registered using {@link #setDataChangeHandler(DataChangeHandler)} + * is informed when new data has been loaded. + * <p> + * After any possible lazy loading and updates are done, the change handler + * is informed that new data is available. + * + * @param firstRowIndex + * the index of the first needed row + * @param numberOfRows + * the number of needed rows + */ + public void ensureAvailability(int firstRowIndex, int numberOfRows); + + /** + * Retrieves the data for the row at the given index. If the row data is not + * available, returns <code>null</code>. + * <p> + * This method does not trigger loading of unavailable data. + * {@link #ensureAvailability(int, int)} should be used to signal what data + * will be needed. + * + * @param rowIndex + * the index of the row to retrieve data for + * @return data for the row; or <code>null</code> if no data is available + */ + public T getRow(int rowIndex); + + /** + * Returns the current best guess for the number of rows in the container. + * + * @return the current estimation of the container size + */ + public int getEstimatedSize(); + + /** + * Sets a data change handler to inform when data is updated, added or + * removed. + * + * @param dataChangeHandler + * the data change handler + */ + public void setDataChangeHandler(DataChangeHandler dataChangeHandler); + + /** + * Gets a {@link RowHandle} of a row object in the cache. + * + * @param row + * the row object for which to retrieve a row handle + * @return a non-<code>null</code> row handle of the given row object + * @throw IllegalStateException if this data source cannot be sure whether + * or not the given row exists. <em>In practice</em> this usually + * means that the row is not currently in this data source's cache. + */ + public RowHandle<T> getHandle(T row); +} diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java new file mode 100644 index 0000000000..2819837504 --- /dev/null +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONParser; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.ui.grid.GridConnector; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; + +/** + * Connects a Vaadin server-side container data source to a Grid. This is + * currently implemented as an Extension hardcoded to support a specific + * connector type. This will be changed once framework support for something + * more flexible has been implemented. + * + * @since + * @author Vaadin Ltd + */ +@Connect(com.vaadin.data.RpcDataProviderExtension.class) +public class RpcDataSourceConnector extends AbstractExtensionConnector { + + public class RpcDataSource extends AbstractRemoteDataSource<JSONObject> { + + private Collection<JSONObject> prevRows = Collections.emptySet(); + + @Override + protected void requestRows(int firstRowIndex, int numberOfRows) { + Range cached = getCachedRange(); + + Collection<JSONObject> newRows = new ArrayList<JSONObject>( + temporarilyPinnedRows); + newRows.removeAll(prevRows); + + List<String> temporarilyPinnedKeys = new ArrayList<String>( + newRows.size()); + for (JSONObject row : newRows) { + temporarilyPinnedKeys.add((String) getRowKey(row)); + } + + getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex, + numberOfRows, cached.getStart(), cached.length(), + temporarilyPinnedKeys); + + prevRows = temporarilyPinnedRows; + } + + @Override + public Object getRowKey(JSONObject row) { + JSONString string = row.get(GridState.JSONKEY_ROWKEY).isString(); + if (string != null) { + return string.stringValue(); + } else { + return null; + } + } + + public RowHandle<JSONObject> getHandleByKey(Object key) { + JSONObject row = new JSONObject(); + row.put(GridState.JSONKEY_ROWKEY, new JSONString((String) key)); + return new RowHandleImpl(row, key); + } + + @Override + @SuppressWarnings("deprecation") + public void transactionPin(Collection<JSONObject> keys) { + super.transactionPin(keys); + if (keys.isEmpty() && !prevRows.isEmpty()) { + prevRows = Collections.emptySet(); + getRpcProxy(DataRequestRpc.class) + .releaseTemporarilyPinnedKeys(); + } + } + } + + private final RpcDataSource dataSource = new RpcDataSource(); + + @Override + protected void extend(ServerConnector target) { + dataSource.setEstimatedSize(getState().containerSize); + ((GridConnector) target).setDataSource(dataSource); + + registerRpc(DataProviderRpc.class, new DataProviderRpc() { + @Override + public void setRowData(int firstRow, String rowsJson) { + JSONValue parsedJson = JSONParser.parseStrict(rowsJson); + JSONArray rowArray = parsedJson.isArray(); + assert rowArray != null : "Was unable to parse JSON into an array: " + + parsedJson; + + ArrayList<JSONObject> rows = new ArrayList<JSONObject>(rowArray + .size()); + for (int i = 0; i < rowArray.size(); i++) { + JSONValue rowValue = rowArray.get(i); + JSONObject rowObject = rowValue.isObject(); + assert rowObject != null : "Was unable to parse JSON into an object: " + + rowValue; + rows.add(rowObject); + } + + dataSource.setRowData(firstRow, rows); + } + + @Override + public void removeRowData(int firstRow, int count) { + dataSource.removeRowData(firstRow, count); + } + + @Override + public void insertRowData(int firstRow, int count) { + dataSource.insertRowData(firstRow, count); + } + }); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.AbstractConnector#getState() + */ + @Override + public DataProviderState getState() { + return (DataProviderState) super.getState(); + } +} diff --git a/client/src/com/vaadin/client/metadata/TypeDataStore.java b/client/src/com/vaadin/client/metadata/TypeDataStore.java index 7aa952d0f2..9b1fd7d45c 100644 --- a/client/src/com/vaadin/client/metadata/TypeDataStore.java +++ b/client/src/com/vaadin/client/metadata/TypeDataStore.java @@ -37,6 +37,8 @@ public class TypeDataStore { .create(); private final FastStringMap<JsArrayString> delegateToWidgetProperties = FastStringMap .create(); + private final FastStringMap<Type> presentationTypes = FastStringMap + .create(); /** * Maps connector class -> state property name -> hander method data @@ -135,6 +137,10 @@ public class TypeDataStore { return get().delegateToWidgetProperties.get(type.getBaseTypeName()); } + public static Type getPresentationType(Class<?> type) { + return get().presentationTypes.get(getType(type).getBaseTypeName()); + } + public void setDelegateToWidget(Class<?> clazz, String propertyName, String delegateValue) { Type type = getType(clazz); @@ -150,6 +156,11 @@ public class TypeDataStore { typeProperties.push(propertyName); } + public void setPresentationType(Class<?> type, Class<?> presentationType) { + presentationTypes.put(getType(type).getBaseTypeName(), + getType(presentationType)); + } + public void setReturnType(Class<?> type, String methodName, Type returnType) { returnTypes.put(new Method(getType(type), methodName).getLookupKey(), returnType); diff --git a/client/src/com/vaadin/client/ui/grid/Cell.java b/client/src/com/vaadin/client/ui/grid/Cell.java new file mode 100644 index 0000000000..ede8bb22d0 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Cell.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.TableCellElement; + +/** + * Describes a cell + * <p> + * It's a representation of the element in a grid cell, and its row and column + * indices. + * <p> + * Unlike the {@link FlyweightRow}, an instance of {@link Cell} can be stored in + * a field. + * + * @since + * @author Vaadin Ltd + */ +public class Cell { + + private final int row; + + private final int column; + + private final TableCellElement element; + + /** + * Constructs a new {@link Cell}. + * + * @param row + * The index of the row + * @param column + * The index of the column + * @param element + * The cell element + */ + public Cell(int row, int column, TableCellElement element) { + super(); + this.row = row; + this.column = column; + this.element = element; + } + + /** + * Returns the index of the row the cell resides in. + * + * @return the row index + * + */ + public int getRow() { + return row; + } + + /** + * Returns the index of the column the cell resides in. + * + * @return the column index + */ + public int getColumn() { + return column; + } + + /** + * Returns the element of the cell. + * + * @return the cell element + */ + public TableCellElement getElement() { + return element; + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java new file mode 100644 index 0000000000..f523fdbbd4 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java @@ -0,0 +1,145 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +/** + * A representation of the columns in an instance of {@link Escalator}. + * + * @since + * @author Vaadin Ltd + * @see Escalator#getColumnConfiguration() + */ +public interface ColumnConfiguration { + + /** + * Removes columns at certain indices. + * <p> + * If any of the removed columns were frozen, the number of frozen columns + * will be reduced by the number of the removed columns that were frozen. + * + * @param index + * the index of the first column to be removed + * @param numberOfColumns + * the number of rows to remove, starting from {@code index} + * @throws IndexOutOfBoundsException + * if the entire range of removed columns is not currently + * present in the escalator + * @throws IllegalArgumentException + * if <code>numberOfColumns</code> is less than 1. + */ + public void removeColumns(int index, int numberOfColumns) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Adds columns at a certain index. + * <p> + * The new columns will be inserted between the column at the index, and the + * column before (an index of 0 means that the columns are inserted at the + * beginning). Therefore, the columns at the index and afterwards will be + * moved to the right. + * <p> + * The contents of the inserted columns will be queried from the respective + * cell renderers in the header, body and footer. + * <p> + * If there are frozen columns and the first added column is to the left of + * the last frozen column, the number of frozen columns will be increased by + * the number of inserted columns. + * <p> + * <em>Note:</em> Only the contents of the inserted columns will be + * rendered. If inserting new columns affects the contents of existing + * columns, {@link RowContainer#refreshRows(int, int)} needs to be called as + * appropriate. + * + * @param index + * the index of the column before which new columns are inserted, + * or {@link #getColumnCount()} to add new columns at the end + * @param numberOfColumns + * the number of columns to insert after the <code>index</code> + * @throws IndexOutOfBoundsException + * if <code>index</code> is not an integer in the range + * <code>[0..{@link #getColumnCount()}]</code> + * @throws IllegalArgumentException + * if {@code numberOfColumns} is less than 1. + */ + public void insertColumns(int index, int numberOfColumns) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Returns the number of columns in the escalator. + * + * @return the number of columns in the escalator + */ + public int getColumnCount(); + + /** + * Sets the number of leftmost columns that are not affected by horizontal + * scrolling. + * + * @param count + * the number of columns to freeze + * + * @throws IllegalArgumentException + * if the column count is < 0 or > the number of columns + * + */ + public void setFrozenColumnCount(int count) throws IllegalArgumentException; + + /** + * Get the number of leftmost columns that are not affected by horizontal + * scrolling. + * + * @return the number of frozen columns + */ + public int getFrozenColumnCount(); + + /** + * Sets (or unsets) an explicit width for a column. + * + * @param index + * the index of the column for which to set a width + * @param px + * the number of pixels the indicated column should be, or a + * negative number to let the escalator decide + * @throws IllegalArgumentException + * if <code>index</code> is not a valid column index + */ + public void setColumnWidth(int index, int px) + throws IllegalArgumentException; + + /** + * Returns the user-defined width of a column. + * + * @param index + * the index of the column for which to retrieve the width + * @return the column's width in pixels, or a negative number if the width + * is implicitly decided by the escalator + * @throws IllegalArgumentException + * if <code>index</code> is not a valid column index + */ + public int getColumnWidth(int index) throws IllegalArgumentException; + + /** + * Returns the actual width of a column. + * + * @param index + * the index of the column for which to retrieve the width + * @return the column's actual width in pixels + * @throws IllegalArgumentException + * if <code>index</code> is not a valid column index + */ + public int getColumnWidthActual(int index) throws IllegalArgumentException; +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/DataAvailableEvent.java b/client/src/com/vaadin/client/ui/grid/DataAvailableEvent.java new file mode 100644 index 0000000000..62b188c0ea --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/DataAvailableEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.shared.ui.grid.Range; + +/** + * Event object describing a change of row availability in DataSource of a Grid. + * + * @since + * @author Vaadin Ltd + */ +public class DataAvailableEvent extends GwtEvent<DataAvailableHandler> { + + private Range rowsAvailable; + public static final Type<DataAvailableHandler> TYPE = new Type<DataAvailableHandler>(); + + public DataAvailableEvent(Range rowsAvailable) { + this.rowsAvailable = rowsAvailable; + } + + /** + * Returns the range of available rows in {@link DataSource} for this event. + * + * @return range of available rows + */ + public Range getAvailableRows() { + return rowsAvailable; + } + + @Override + public Type<DataAvailableHandler> getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(DataAvailableHandler handler) { + handler.onDataAvailable(this); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java b/client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java new file mode 100644 index 0000000000..06ea08a17e --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for {@link DataAvailableEvent}s. + * + * @since + * @author Vaadin Ltd + */ +public interface DataAvailableHandler extends EventHandler { + + /** + * Called when DataSource has data available. Supplied with row range. + * + * @param availableRows + * Range of rows available in the DataSource + * @return true if the command was successfully completed, false to call + * again the next time new data is available + */ + public void onDataAvailable(DataAvailableEvent event); +} diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java new file mode 100644 index 0000000000..bf62805034 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -0,0 +1,4626 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.gwt.animation.client.AnimationScheduler; +import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; +import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.logging.client.LogConfiguration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.Profiler; +import com.vaadin.client.Util; +import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; +import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition; +import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition; +import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition; +import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; +import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; +import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.util.SharedUtil; + +/*- + + Maintenance Notes! Reading these might save your day. + (note for editors: line width is 80 chars, including the + one-space indentation) + + + == Row Container Structure + + AbstractRowContainer + |-- AbstractStaticRowContainer + | |-- HeaderRowContainer + | `-- FooterContainer + `---- BodyRowContainer + + AbstractRowContainer is intended to contain all common logic + between RowContainers. It manages the bookkeeping of row + count, makes sure that all individual cells are rendered + the same way, and so on. + + AbstractStaticRowContainer has some special logic that is + required by all RowContainers that don't scroll (hence the + word "static"). HeaderRowContainer and FooterRowContainer + are pretty thin special cases of a StaticRowContainer + (mostly relating to positioning of the root element). + + BodyRowContainer could also be split into an additional + "AbstractScrollingRowContainer", but I felt that no more + inner classes were needed. So it contains both logic + required for making things scroll about, and equivalent + special cases for layouting, as are found in + Header/FooterRowContainers. + + + == The Three Indices + + Each RowContainer can be thought to have three levels of + indices for any given displayed row (but the distinction + matters primarily for the BodyRowContainer, because of the + way it scrolls through data): + + - Logical index + - Physical (or DOM) index + - Visual index + + LOGICAL INDEX is the index that is linked to the data + source. If you want your data source to represent a SQL + database with 10 000 rows, the 7 000:th row in the SQL has a + logical index of 6 999, since the index is 0-based (unless + that data source does some funky logic). + + PHYSICAL INDEX is the index for a row that you see in a + browser's DOM inspector. If your row is the second <tr> + element within a <tbody> tag, it has a physical index of 1 + (because of 0-based indices). In Header and + FooterRowContainers, you are safe to assume that the logical + index is the same as the physical index. But because the + BodyRowContainer never displays large data sources entirely + in the DOM, a physical index usually has no apparent direct + relationship with its logical index. + + VISUAL INDEX is the index relating to the order that you + see a row in, in the browser, as it is rendered. The + topmost row is 0, the second is 1, and so on. The visual + index is similar to the physical index in the sense that + Header and FooterRowContainers can assume a 1:1 + relationship between visual index and logical index. And + again, BodyRowContainer has no such relationship. The + body's visual index has additionally no apparent + relationship with its physical index. Because the <tr> tags + are reused in the body and visually repositioned with CSS + as the user scrolls, the relationship between physical + index and visual index is quickly broken. You can get an + element's visual index via the field + BodyRowContainer.visualRowOrder. + + Currently, the physical and visual indices are kept in sync + _most of the time_ by a deferred rearrangement of rows. + They become desynced when scrolling. This is to help screen + readers to read the contents from the DOM in a natural + order. See BodyRowContainer.DeferredDomSorter for more + about that. + + */ + +/** + * A workaround-class for GWT and JSNI. + * <p> + * GWT is unable to handle some method calls to Java methods in inner-classes + * from within JSNI blocks. Having that inner class extend a non-inner-class (or + * implement such an interface), makes it possible for JSNI to indirectly refer + * to the inner class, by invoking methods and fields in the non-inner-class + * API. + * + * @see Escalator.Scroller + */ +abstract class JsniWorkaround { + /** + * A JavaScript function that handles the scroll DOM event, and passes it on + * to Java code. + * + * @see #createScrollListenerFunction(Escalator) + * @see Escalator#onScroll() + * @see Escalator.Scroller#onScroll() + */ + protected final JavaScriptObject scrollListenerFunction; + + /** + * A JavaScript function that handles the mousewheel DOM event, and passes + * it on to Java code. + * + * @see #createMousewheelListenerFunction(Escalator) + * @see Escalator#onScroll() + * @see Escalator.Scroller#onScroll() + */ + protected final JavaScriptObject mousewheelListenerFunction; + + /** + * A JavaScript function that handles the touch start DOM event, and passes + * it on to Java code. + * + * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) + */ + protected JavaScriptObject touchStartFunction; + + /** + * A JavaScript function that handles the touch move DOM event, and passes + * it on to Java code. + * + * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) + */ + protected JavaScriptObject touchMoveFunction; + + /** + * A JavaScript function that handles the touch end and cancel DOM events, + * and passes them on to Java code. + * + * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) + */ + protected JavaScriptObject touchEndFunction; + + protected TouchHandlerBundle touchHandlerBundle; + + protected JsniWorkaround(final Escalator escalator) { + scrollListenerFunction = createScrollListenerFunction(escalator); + mousewheelListenerFunction = createMousewheelListenerFunction(escalator); + + touchHandlerBundle = new TouchHandlerBundle(escalator); + touchStartFunction = touchHandlerBundle.getTouchStartHandler(); + touchMoveFunction = touchHandlerBundle.getTouchMoveHandler(); + touchEndFunction = touchHandlerBundle.getTouchEndHandler(); + } + + /** + * A method that constructs the JavaScript function that will be stored into + * {@link #scrollListenerFunction}. + * + * @param esc + * a reference to the current instance of {@link Escalator} + * @see Escalator#onScroll() + */ + protected abstract JavaScriptObject createScrollListenerFunction( + Escalator esc); + + /** + * A method that constructs the JavaScript function that will be stored into + * {@link #mousewheelListenerFunction}. + * + * @param esc + * a reference to the current instance of {@link Escalator} + * @see Escalator#onScroll() + */ + protected abstract JavaScriptObject createMousewheelListenerFunction( + Escalator esc); +} + +/** + * A low-level table-like widget that features a scrolling virtual viewport and + * lazily generated rows. + * + * @since + * @author Vaadin Ltd + */ +public class Escalator extends Widget { + + // todo comments legend + /* + * [[optimize]]: There's an opportunity to rewrite the code in such a way + * that it _might_ perform better (rememeber to measure, implement, + * re-measure) + */ + /* + * [[rowheight]]: This code will require alterations that are relevant for + * being able to support variable row heights. NOTE: these bits can most + * often also be identified by searching for code reading the ROW_HEIGHT_PX + * constant. + */ + /* + * [[mpixscroll]]: This code will require alterations that are relevant for + * supporting the scrolling through more pixels than some browsers normally + * would support. (i.e. when we support more than "a million" pixels in the + * escalator DOM). NOTE: these bits can most often also be identified by + * searching for code that call scrollElem.getScrollTop();. + */ + + /** + * A utility class that contains utility methods that are usually called + * from JSNI. + * <p> + * The methods are moved in this class to minimize the amount of JSNI code + * as much as feasible. + */ + static class JsniUtil { + public static class TouchHandlerBundle { + + /** + * A <a href= + * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html" + * >JavaScriptObject overlay</a> for the <a + * href="http://www.w3.org/TR/touch-events/">JavaScript + * TouchEvent</a> object. + * <p> + * This needs to be used in the touch event handlers, since GWT's + * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent} + * can't be cast from the JSNI call, and the + * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't + * properly populated with the correct values. + */ + private final static class CustomTouchEvent extends + JavaScriptObject { + protected CustomTouchEvent() { + } + + public native NativeEvent getNativeEvent() + /*-{ + return this; + }-*/; + + public native int getPageX() + /*-{ + return this.targetTouches[0].pageX; + }-*/; + + public native int getPageY() + /*-{ + return this.targetTouches[0].pageY; + }-*/; + } + + private double touches = 0; + private int lastX = 0; + private int lastY = 0; + private double lastTime = 0; + private boolean snappedScrollEnabled = true; + private double deltaX = 0; + private double deltaY = 0; + + private final Escalator escalator; + private CustomTouchEvent latestTouchMoveEvent; + private AnimationCallback mover = new AnimationCallback() { + @Override + public void execute(double doNotUseThisTimestamp) { + /* + * We can't use the timestamp parameter here, since it is + * not in any predetermined format; TouchEnd does not + * provide a compatible timestamp, and we need to be able to + * get a comparable timestamp to determine whether to + * trigger a flick scroll or not. + */ + + if (touches != 1) { + return; + } + + final int x = latestTouchMoveEvent.getPageX(); + final int y = latestTouchMoveEvent.getPageY(); + deltaX = x - lastX; + deltaY = y - lastY; + lastX = x; + lastY = y; + + /* + * Instead of using the provided arbitrary timestamp, let's + * use a known-format and reproducible timestamp. + */ + lastTime = Duration.currentTimeMillis(); + + // snap the scroll to the major axes, at first. + if (snappedScrollEnabled) { + final double oldDeltaX = deltaX; + final double oldDeltaY = deltaY; + + /* + * Scrolling snaps to 40 degrees vs. flick scroll's 30 + * degrees, since slow movements have poor resolution - + * it's easy to interpret a slight angle as a steep + * angle, since the sample rate is "unnecessarily" high. + * 40 simply felt better than 30. + */ + final double[] snapped = Escalator.snapDeltas(deltaX, + deltaY, RATIO_OF_40_DEGREES); + deltaX = snapped[0]; + deltaY = snapped[1]; + + /* + * if the snap failed once, let's follow the pointer + * from now on. + */ + if (oldDeltaX != 0 && deltaX == oldDeltaX + && oldDeltaY != 0 && deltaY == oldDeltaY) { + snappedScrollEnabled = false; + } + } + + moveScrollFromEvent(escalator, -deltaX, -deltaY, + latestTouchMoveEvent.getNativeEvent()); + } + }; + private AnimationHandle animationHandle; + + public TouchHandlerBundle(final Escalator escalator) { + this.escalator = escalator; + } + + public native JavaScriptObject getTouchStartHandler() + /*-{ + // we need to store "this", since it won't be preserved on call. + var self = this; + return $entry(function (e) { + self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e); + }); + }-*/; + + public native JavaScriptObject getTouchMoveHandler() + /*-{ + // we need to store "this", since it won't be preserved on call. + var self = this; + return $entry(function (e) { + self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e); + }); + }-*/; + + public native JavaScriptObject getTouchEndHandler() + /*-{ + // we need to store "this", since it won't be preserved on call. + var self = this; + return $entry(function (e) { + self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e); + }); + }-*/; + + public void touchStart(final CustomTouchEvent event) { + touches = event.getNativeEvent().getTouches().length(); + if (touches != 1) { + return; + } + + escalator.scroller.cancelFlickScroll(); + + lastX = event.getPageX(); + lastY = event.getPageY(); + + snappedScrollEnabled = true; + } + + public void touchMove(final CustomTouchEvent event) { + /* + * since we only use the getPageX/Y, and calculate the diff + * within the handler, we don't need to calculate any + * intermediate deltas. + */ + latestTouchMoveEvent = event; + + if (animationHandle != null) { + animationHandle.cancel(); + } + animationHandle = AnimationScheduler.get() + .requestAnimationFrame(mover, escalator.bodyElem); + event.getNativeEvent().preventDefault(); + + /* + * this initializes a correct timestamp, and also renders the + * first frame for added responsiveness. + */ + mover.execute(Duration.currentTimeMillis()); + } + + public void touchEnd(final CustomTouchEvent event) { + touches = event.getNativeEvent().getTouches().length(); + + if (touches == 0) { + escalator.scroller.handleFlickScroll(deltaX, deltaY, + lastTime); + escalator.body.domSorter.reschedule(); + } + } + } + + public static void moveScrollFromEvent(final Escalator escalator, + final double deltaX, final double deltaY, + final NativeEvent event) { + + if (!Double.isNaN(deltaX)) { + escalator.horizontalScrollbar.setScrollPosByDelta(deltaX); + } + + if (!Double.isNaN(deltaY)) { + escalator.verticalScrollbar.setScrollPosByDelta(deltaY); + } + + /* + * TODO: only prevent if not scrolled to end/bottom. Or no? UX team + * needs to decide. + */ + final boolean warrantedYScroll = deltaY != 0 + && escalator.verticalScrollbar.showsScrollHandle(); + final boolean warrantedXScroll = deltaX != 0 + && escalator.horizontalScrollbar.showsScrollHandle(); + if (warrantedYScroll || warrantedXScroll) { + event.preventDefault(); + } + } + } + + /** + * The animation callback that handles the animation of a touch-scrolling + * flick with inertia. + */ + private class FlickScrollAnimator implements AnimationCallback { + private static final double MIN_MAGNITUDE = 0.005; + private static final double MAX_SPEED = 7; + + private double velX; + private double velY; + private double prevTime = 0; + private int millisLeft; + private double xFric; + private double yFric; + + private boolean cancelled = false; + private double lastLeft; + private double lastTop; + + /** + * Creates a new animation callback to handle touch-scrolling flick with + * inertia. + * + * @param deltaX + * the last scrolling delta in the x-axis in a touchmove + * @param deltaY + * the last scrolling delta in the y-axis in a touchmove + * @param lastTime + * the timestamp of the last touchmove + */ + public FlickScrollAnimator(final double deltaX, final double deltaY, + final double lastTime) { + final double currentTimeMillis = Duration.currentTimeMillis(); + velX = Math.max(Math.min(deltaX / (currentTimeMillis - lastTime), + MAX_SPEED), -MAX_SPEED); + velY = Math.max(Math.min(deltaY / (currentTimeMillis - lastTime), + MAX_SPEED), -MAX_SPEED); + + lastLeft = horizontalScrollbar.getScrollPos(); + lastTop = verticalScrollbar.getScrollPos(); + + /* + * If we're scrolling mainly in one of the four major directions, + * and only a teeny bit to any other side, snap the scroll to that + * major direction instead. + */ + final double[] snapDeltas = Escalator.snapDeltas(velX, velY, + RATIO_OF_30_DEGREES); + velX = snapDeltas[0]; + velY = snapDeltas[1]; + + if (velX * velX + velY * velY > MIN_MAGNITUDE) { + millisLeft = 1500; + xFric = velX / millisLeft; + yFric = velY / millisLeft; + } else { + millisLeft = 0; + } + + } + + @Override + public void execute(final double doNotUseThisTimestamp) { + /* + * We cannot use the timestamp provided to this method since it is + * of a format that cannot be determined at will. Therefore, we need + * a timestamp format that we can handle, so our calculations are + * correct. + */ + + if (millisLeft <= 0 || cancelled) { + scroller.currentFlickScroller = null; + return; + } + + final double timestamp = Duration.currentTimeMillis(); + if (prevTime == 0) { + prevTime = timestamp; + AnimationScheduler.get().requestAnimationFrame(this); + return; + } + + double currentLeft = horizontalScrollbar.getScrollPos(); + double currentTop = verticalScrollbar.getScrollPos(); + + final double timeDiff = timestamp - prevTime; + double left = currentLeft - velX * timeDiff; + setScrollLeft(left); + velX -= xFric * timeDiff; + + double top = currentTop - velY * timeDiff; + setScrollTop(top); + velY -= yFric * timeDiff; + + cancelBecauseOfEdgeOrCornerMaybe(); + + prevTime = timestamp; + millisLeft -= timeDiff; + lastLeft = currentLeft; + lastTop = currentTop; + AnimationScheduler.get().requestAnimationFrame(this); + } + + private void cancelBecauseOfEdgeOrCornerMaybe() { + if (lastLeft == horizontalScrollbar.getScrollPos() + && lastTop == verticalScrollbar.getScrollPos()) { + cancel(); + } + } + + public void cancel() { + cancelled = true; + } + } + + /** + * ScrollDestination case-specific handling logic. + */ + private static double getScrollPos(final ScrollDestination destination, + final double targetStartPx, final double targetEndPx, + final double viewportStartPx, final double viewportEndPx, + final int padding) { + + final double viewportLength = viewportEndPx - viewportStartPx; + + switch (destination) { + + /* + * Scroll as little as possible to show the target element. If the + * element fits into view, this works as START or END depending on the + * current scroll position. If the element does not fit into view, this + * works as START. + */ + case ANY: { + final double startScrollPos = targetStartPx - padding; + final double endScrollPos = targetEndPx + padding - viewportLength; + + if (startScrollPos < viewportStartPx) { + return startScrollPos; + } else if (targetEndPx + padding > viewportEndPx) { + return endScrollPos; + } else { + // NOOP, it's already visible + return viewportStartPx; + } + } + + /* + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + case END: { + return targetEndPx + padding - viewportLength; + } + + /* + * Scrolls so that the element is shown in the middle of the viewport. + * The viewport will, however, not scroll beyond its contents, given + * more elements than what the viewport is able to show at once. Under + * no circumstances will the viewport scroll before its first element. + */ + case MIDDLE: { + final double targetMiddle = targetStartPx + + (targetEndPx - targetStartPx) / 2; + return targetMiddle - viewportLength / 2; + } + + /* + * Scrolls so that the element is shown at the start of the viewport. + * The viewport will, however, not scroll beyond its contents. + */ + case START: { + return targetStartPx - padding; + } + + /* + * Throw an error if we're here. This can only mean that + * ScrollDestination has been carelessly amended.. + */ + default: { + throw new IllegalArgumentException( + "Internal: ScrollDestination has been modified, " + + "but Escalator.getScrollPos has not been updated " + + "to match new values."); + } + } + + } + + /** An inner class that handles all logic related to scrolling. */ + private class Scroller extends JsniWorkaround { + private double lastScrollTop = 0; + private double lastScrollLeft = 0; + /** + * The current flick scroll animator. This is <code>null</code> if the + * view isn't animating a flick scroll at the moment. + */ + private FlickScrollAnimator currentFlickScroller; + + public Scroller() { + super(Escalator.this); + } + + @Override + protected native JavaScriptObject createScrollListenerFunction( + Escalator esc) + /*-{ + var vScroll = esc.@com.vaadin.client.ui.grid.Escalator::verticalScrollbar; + var vScrollElem = vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()(); + + var hScroll = esc.@com.vaadin.client.ui.grid.Escalator::horizontalScrollbar; + var hScrollElem = hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()(); + + return $entry(function(e) { + var target = e.target || e.srcElement; // IE8 uses e.scrElement + + // in case the scroll event was native (i.e. scrollbars were dragged, or + // the scrollTop/Left was manually modified), the bundles have old cache + // values. We need to make sure that the caches are kept up to date. + if (target === vScrollElem) { + vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()(); + } else if (target === hScrollElem) { + hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()(); + } else { + $wnd.console.error("unexpected scroll target: "+target); + } + + esc.@com.vaadin.client.ui.grid.Escalator::onScroll()(); + }); + }-*/; + + @Override + protected native JavaScriptObject createMousewheelListenerFunction( + Escalator esc) + /*-{ + return $entry(function(e) { + var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX; + var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY; + + // IE8 has only delta y + if (isNaN(deltaY)) { + deltaY = -0.5*e.wheelDelta; + } + + @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e); + }); + }-*/; + + /** + * Recalculates the virtual viewport represented by the scrollbars, so + * that the sizes of the scroll handles appear correct in the browser + */ + public void recalculateScrollbarsForVirtualViewport() { + int scrollContentHeight = body.calculateEstimatedTotalRowHeight(); + int scrollContentWidth = columnConfiguration.calculateRowWidth(); + + double tableWrapperHeight = heightOfEscalator; + double tableWrapperWidth = widthOfEscalator; + + boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + - header.heightOfSection - footer.heightOfSection; + boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth; + + // One dimension got scrollbars, but not the other. Recheck time! + if (verticalScrollNeeded != horizontalScrollNeeded) { + if (!verticalScrollNeeded && horizontalScrollNeeded) { + verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + - header.heightOfSection + - footer.heightOfSection + - horizontalScrollbar.getScrollbarThickness(); + } else { + horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth + - verticalScrollbar.getScrollbarThickness(); + } + } + + // let's fix the table wrapper size, since it's now stable. + if (verticalScrollNeeded) { + tableWrapperWidth -= verticalScrollbar.getScrollbarThickness(); + } + if (horizontalScrollNeeded) { + tableWrapperHeight -= horizontalScrollbar + .getScrollbarThickness(); + } + tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX); + tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX); + + verticalScrollbar.setOffsetSize(tableWrapperHeight + - footer.heightOfSection - header.heightOfSection); + verticalScrollbar.setScrollSize(scrollContentHeight); + + /* + * If decreasing the amount of frozen columns, and scrolled to the + * right, the scroll position might reset. So we need to remember + * the scroll position, and re-apply it once the scrollbar size has + * been adjusted. + */ + double prevScrollPos = horizontalScrollbar.getScrollPos(); + + int unfrozenPixels = columnConfiguration + .getCalculatedColumnsWidth(Range.between( + columnConfiguration.getFrozenColumnCount(), + columnConfiguration.getColumnCount())); + int frozenPixels = scrollContentWidth - unfrozenPixels; + double hScrollOffsetWidth = tableWrapperWidth - frozenPixels; + horizontalScrollbar.setOffsetSize(hScrollOffsetWidth); + horizontalScrollbar.setScrollSize(unfrozenPixels); + horizontalScrollbar.getElement().getStyle() + .setLeft(frozenPixels, Unit.PX); + horizontalScrollbar.setScrollPos(prevScrollPos); + } + + /** + * Logical scrolling event handler for the entire widget. + */ + public void onScroll() { + if (internalScrollEventCalls > 0) { + internalScrollEventCalls--; + return; + } + + final double scrollTop = verticalScrollbar.getScrollPos(); + final double scrollLeft = horizontalScrollbar.getScrollPos(); + if (lastScrollLeft != scrollLeft) { + for (int i = 0; i < columnConfiguration.frozenColumns; i++) { + header.updateFreezePosition(i, scrollLeft); + body.updateFreezePosition(i, scrollLeft); + footer.updateFreezePosition(i, scrollLeft); + } + + position.set(headElem, -scrollLeft, 0); + + /* + * TODO [[optimize]]: cache this value in case the instanceof + * check has undesirable overhead. This could also be a + * candidate for some deferred binding magic so that e.g. + * AbsolutePosition is not even considered in permutations that + * we know support something better. That would let the compiler + * completely remove the entire condition since it knows that + * the if will never be true. + */ + if (position instanceof AbsolutePosition) { + /* + * we don't want to put "top: 0" on the footer, since it'll + * render wrong, as we already have + * "bottom: $footer-height". + */ + footElem.getStyle().setLeft(-scrollLeft, Unit.PX); + } else { + position.set(footElem, -scrollLeft, 0); + } + + lastScrollLeft = scrollLeft; + } + + body.setBodyScrollPosition(scrollLeft, scrollTop); + + lastScrollTop = scrollTop; + body.updateEscalatorRowsOnScroll(); + /* + * TODO [[optimize]]: Might avoid a reflow by first calculating new + * scrolltop and scrolleft, then doing the escalator magic based on + * those numbers and only updating the positions after that. + */ + } + + public native void attachScrollListener(Element element) + /* + * Attaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + element.addEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } else { + element.attachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } + }-*/; + + public native void detachScrollListener(Element element) + /* + * Attaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + element.removeEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } else { + element.detachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } + }-*/; + + public native void attachMousewheelListener(Element element) + /* + * Attaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + // firefox likes "wheel", while others use "mousewheel" + var eventName = element.onwheel===undefined?"mousewheel":"wheel"; + element.addEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } else { + // IE8 + element.attachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } + }-*/; + + public native void detachMousewheelListener(Element element) + /* + * Detaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + // firefox likes "wheel", while others use "mousewheel" + var eventName = element.onwheel===undefined?"mousewheel":"wheel"; + element.removeEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } else { + // IE8 + element.detachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } + }-*/; + + public native void attachTouchListeners(Element element) + /* + * Detaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + element.addEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction); + element.addEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction); + element.addEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + element.addEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + } else { + // this would be IE8, but we don't support it with touch + } + }-*/; + + public native void detachTouchListeners(Element element) + /* + * Detaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.removeEventListener) { + element.removeEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction); + element.removeEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction); + element.removeEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + element.removeEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + } else { + // this would be IE8, but we don't support it with touch + } + }-*/; + + private void cancelFlickScroll() { + if (currentFlickScroller != null) { + currentFlickScroller.cancel(); + } + } + + /** + * Handles a touch-based flick scroll. + * + * @param deltaX + * the last scrolling delta in the x-axis in a touchmove + * @param deltaY + * the last scrolling delta in the y-axis in a touchmove + * @param lastTime + * the timestamp of the last touchmove + */ + public void handleFlickScroll(double deltaX, double deltaY, + double lastTime) { + currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY, + lastTime); + AnimationScheduler.get() + .requestAnimationFrame(currentFlickScroller); + } + + public void scrollToColumn(final int columnIndex, + final ScrollDestination destination, final int padding) { + assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column"; + + /* + * To cope with frozen columns, we just pretend those columns are + * not there at all when calculating the position of the target + * column and the boundaries of the viewport. The resulting + * scrollLeft will be correct without compensation since the DOM + * structure effectively means that scrollLeft also ignores the + * frozen columns. + */ + final int frozenPixels = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, + columnConfiguration.frozenColumns)); + + final int targetStartPx = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, columnIndex)) + - frozenPixels; + final int targetEndPx = targetStartPx + + columnConfiguration.getColumnWidthActual(columnIndex); + + final double viewportStartPx = getScrollLeft(); + double viewportEndPx = viewportStartPx + + getElement().getOffsetWidth() - frozenPixels; + if (verticalScrollbar.showsScrollHandle()) { + viewportEndPx -= Util.getNativeScrollbarSize(); + } + + final double scrollLeft = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + /* + * note that it doesn't matter if the scroll would go beyond the + * content, since the browser will adjust for that, and everything + * fall into line accordingly. + */ + setScrollLeft(scrollLeft); + } + + public void scrollToRow(final int rowIndex, + final ScrollDestination destination, final int padding) { + /* + * FIXME [[rowheight]]: coded to work only with default row heights + * - will not work with variable row heights + */ + final int targetStartPx = body.getDefaultRowHeight() * rowIndex; + final int targetEndPx = targetStartPx + body.getDefaultRowHeight(); + + final double viewportStartPx = getScrollTop(); + final double viewportEndPx = viewportStartPx + + body.calculateHeight(); + + final double scrollTop = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + /* + * note that it doesn't matter if the scroll would go beyond the + * content, since the browser will adjust for that, and everything + * falls into line accordingly. + */ + setScrollTop(scrollTop); + } + } + + private abstract class AbstractRowContainer implements RowContainer { + + private EscalatorUpdater updater = EscalatorUpdater.NULL; + + private int rows; + + /** + * The table section element ({@code <thead>}, {@code <tbody>} or + * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in. + */ + protected final TableSectionElement root; + + /** The height of the combined rows in the DOM. */ + protected double heightOfSection = -1; + + /** + * 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, Integer> rowTopPositionMap = new HashMap<TableRowElement, Integer>(); + + private boolean defaultRowHeightShouldBeAutodetected = true; + + private int defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; + + public AbstractRowContainer( + final TableSectionElement rowContainerElement) { + root = rowContainerElement; + } + + @Override + public Element getElement() { + return root; + } + + /** + * Gets the tag name of an element to represent a cell in a row. + * <p> + * Usually {@code "th"} or {@code "td"}. + * <p> + * <em>Note:</em> To actually <em>create</em> such an element, use + * {@link #createCellElement(int, int)} instead. + * + * @return the tag name for the element to represent cells as + * @see #createCellElement(int, int) + */ + protected abstract String getCellElementTagName(); + + @Override + public EscalatorUpdater getEscalatorUpdater() { + return updater; + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for rows or columns + * when this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) { + if (escalatorUpdater == null) { + throw new IllegalArgumentException( + "escalator updater cannot be null"); + } + + updater = escalatorUpdater; + + if (hasColumnAndRowData() && getRowCount() > 0) { + refreshRows(0, getRowCount()); + } + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there are no rows in the DOM when + * this method is called. + * + * @see #hasSomethingInDom() + */ + @Override + public void removeRows(final int index, final int numberOfRows) { + assertArgumentsAreValidAndWithinRange(index, numberOfRows); + + rows -= numberOfRows; + + if (!isAttached()) { + return; + } + + if (hasSomethingInDom()) { + paintRemoveRows(index, numberOfRows); + } + } + + /** + * Removes those row elements from the DOM that correspond to the given + * range of logical indices. This may be fewer than {@code numberOfRows} + * , even zero, if not all the removed rows are actually visible. + * <p> + * The implementation must call {@link #paintRemoveRow(Element, int)} + * for each row that is removed from the DOM. + * + * @param index + * the logical index of the first removed row + * @param numberOfRows + * number of logical rows to remove + */ + protected abstract void paintRemoveRows(final int index, + final int numberOfRows); + + /** + * Removes a row element from the DOM, invoking + * {@link #getEscalatorUpdater()} + * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and + * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before + * and after removing the row, respectively. + * <p> + * This method must be called for each removed DOM row by any + * {@link #paintRemoveRows(int, int)} implementation. + * + * @param tr + * the row element to remove. + */ + protected void paintRemoveRow(final TableRowElement tr, + final int logicalRowIndex) { + + flyweightRow.setup(tr, logicalRowIndex, + columnConfiguration.getCalculatedColumnWidths()); + + getEscalatorUpdater().preDetach(flyweightRow, + flyweightRow.getCells()); + + tr.removeFromParent(); + + getEscalatorUpdater().postDetach(flyweightRow, + flyweightRow.getCells()); + + /* + * the "assert" guarantees that this code is run only during + * development/debugging. + */ + assert flyweightRow.teardown(); + + } + + private void assertArgumentsAreValidAndWithinRange(final int index, + final int numberOfRows) throws IllegalArgumentException, + IndexOutOfBoundsException { + if (numberOfRows < 1) { + throw new IllegalArgumentException( + "Number of rows must be 1 or greater (was " + + numberOfRows + ")"); + } + + if (index < 0 || index + numberOfRows > getRowCount()) { + throw new IndexOutOfBoundsException("The given " + + "row range (" + index + ".." + (index + numberOfRows) + + ") was outside of the current number of rows (" + + getRowCount() + ")"); + } + } + + @Override + public int getRowCount() { + return rows; + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for columns when + * this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void insertRows(final int index, final int numberOfRows) { + if (index < 0 || index > getRowCount()) { + throw new IndexOutOfBoundsException("The given index (" + index + + ") was outside of the current number of rows (0.." + + getRowCount() + ")"); + } + + if (numberOfRows < 1) { + throw new IllegalArgumentException( + "Number of rows must be 1 or greater (was " + + numberOfRows + ")"); + } + + rows += numberOfRows; + + /* + * only add items in the DOM if the widget itself is attached to the + * DOM. We can't calculate sizes otherwise. + */ + if (isAttached()) { + paintInsertRows(index, numberOfRows); + } + } + + /** + * Actually add rows into the DOM, now that everything can be + * calculated. + * + * @param visualIndex + * the DOM index to add rows into + * @param numberOfRows + * the number of rows to insert + * @return a list of the added row elements + */ + protected List<TableRowElement> paintInsertRows(final int visualIndex, + final int numberOfRows) { + assert isAttached() : "Can't paint rows if Escalator is not attached"; + + final List<TableRowElement> addedRows = new ArrayList<TableRowElement>(); + + if (numberOfRows < 1) { + return addedRows; + } + + Node referenceRow; + if (root.getChildCount() != 0 && visualIndex != 0) { + // get the row node we're inserting stuff after + referenceRow = root.getChild(visualIndex - 1); + } else { + // index is 0, so just prepend. + referenceRow = null; + } + + for (int row = visualIndex; row < visualIndex + numberOfRows; row++) { + final int rowHeight = getDefaultRowHeight(); + final TableRowElement tr = TableRowElement.as(DOM.createTR()); + addedRows.add(tr); + tr.addClassName(getStylePrimaryName() + "-row"); + + for (int col = 0; col < columnConfiguration.getColumnCount(); col++) { + final int colWidth = columnConfiguration + .getColumnWidthActual(col); + final TableCellElement cellElem = createCellElement( + rowHeight, colWidth); + tr.appendChild(cellElem); + + // Set stylename and position if new cell is frozen + if (col < columnConfiguration.frozenColumns) { + cellElem.addClassName("frozen"); + position.set(cellElem, scroller.lastScrollLeft, 0); + } + } + + referenceRow = paintInsertRow(referenceRow, tr, row); + } + reapplyRowWidths(); + + recalculateSectionHeight(); + + return addedRows; + } + + /** + * Inserts a single row into the DOM, invoking + * {@link #getEscalatorUpdater()} + * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and + * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before + * and after inserting the row, respectively. The row should have its + * cells already inserted. + * + * @param referenceRow + * the row after which to insert or null if insert as first + * @param tr + * the row to be inserted + * @param logicalRowIndex + * the logical index of the inserted row + * @return the inserted row to be used as the new reference + */ + protected Node paintInsertRow(Node referenceRow, + final TableRowElement tr, int logicalRowIndex) { + flyweightRow.setup(tr, logicalRowIndex, + columnConfiguration.getCalculatedColumnWidths()); + + getEscalatorUpdater().preAttach(flyweightRow, + flyweightRow.getCells()); + + referenceRow = insertAfterReferenceAndUpdateIt(root, tr, + referenceRow); + + getEscalatorUpdater().postAttach(flyweightRow, + flyweightRow.getCells()); + updater.update(flyweightRow, flyweightRow.getCells()); + + /* + * the "assert" guarantees that this code is run only during + * development/debugging. + */ + assert flyweightRow.teardown(); + return referenceRow; + } + + private Node insertAfterReferenceAndUpdateIt(final Element parent, + final Element elem, final Node referenceNode) { + if (referenceNode != null) { + parent.insertAfter(elem, referenceNode); + } else { + /* + * referencenode being null means we have offset 0, i.e. make it + * the first row + */ + /* + * TODO [[optimize]]: Is insertFirst or append faster for an + * empty root? + */ + parent.insertFirst(elem); + } + return elem; + } + + abstract protected void recalculateSectionHeight(); + + /** + * Returns the estimated height of all rows in the row container. + * <p> + * The estimate is promised to be correct as long as there are no rows + * with calculated heights. + */ + protected int calculateEstimatedTotalRowHeight() { + return getDefaultRowHeight() * getRowCount(); + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for columns when + * this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void refreshRows(final int index, final int numberOfRows) { + Profiler.enter("Escalator.AbstractRowContainer.refreshRows"); + + assertArgumentsAreValidAndWithinRange(index, numberOfRows); + + if (!isAttached()) { + return; + } + + /* + * TODO [[rowheight]]: even if no rows are evaluated in the current + * viewport, the heights of some unrendered rows might change in a + * refresh. This would cause the scrollbar to be adjusted (in + * scrollHeight and/or scrollTop). Do we want to take this into + * account? + */ + if (hasColumnAndRowData()) { + /* + * TODO [[rowheight]]: nudge rows down with + * refreshRowPositions() as needed + */ + for (int row = index; row < index + numberOfRows; row++) { + final TableRowElement tr = getTrByVisualIndex(row); + refreshRow(tr, row); + } + } + + Profiler.leave("Escalator.AbstractRowContainer.refreshRows"); + } + + void refreshRow(final TableRowElement tr, final int logicalRowIndex) { + flyweightRow.setup(tr, logicalRowIndex, + columnConfiguration.getCalculatedColumnWidths()); + updater.update(flyweightRow, flyweightRow.getCells()); + + /* + * the "assert" guarantees that this code is run only during + * development/debugging. + */ + assert flyweightRow.teardown(); + } + + /** + * Create and setup an empty cell element. + * + * @param width + * the width of the cell, in pixels + * @param height + * the height of the cell, in pixels + * + * @return a set-up empty cell element + */ + @SuppressWarnings("hiding") + public TableCellElement createCellElement(final int height, + final int width) { + final TableCellElement cellElem = TableCellElement.as(DOM + .createElement(getCellElementTagName())); + cellElem.getStyle().setHeight(height, Unit.PX); + cellElem.getStyle().setWidth(width, Unit.PX); + cellElem.addClassName(getStylePrimaryName() + "-cell"); + return cellElem; + } + + @Override + public TableRowElement getRowElement(int index) { + return getTrByVisualIndex(index); + } + + /** + * Gets the child element that is visually at a certain index + * + * @param index + * the index of the element to retrieve + * @return the element at position {@code index} + * @throws IndexOutOfBoundsException + * if {@code index} is not valid within {@link #root} + */ + protected abstract TableRowElement getTrByVisualIndex(int index) + throws IndexOutOfBoundsException; + + protected void paintRemoveColumns(final int offset, + final int numberOfColumns, + final List<ColumnConfigurationImpl.Column> removedColumns) { + final NodeList<Node> childNodes = root.getChildNodes(); + for (int visualRowIndex = 0; visualRowIndex < childNodes + .getLength(); visualRowIndex++) { + final TableRowElement tr = getTrByVisualIndex(visualRowIndex); + + flyweightRow.setup(tr, visualRowIndex, + columnConfiguration.getCalculatedColumnWidths()); + + Iterable<FlyweightCell> cells = flyweightRow.getCells(offset, + numberOfColumns); + + getEscalatorUpdater().preDetach(flyweightRow, cells); + + for (FlyweightCell cell : cells) { + Element cellElement = cell.getElement(); + cellElement.removeFromParent(); + } + + /** + * We need a new iterable that does not try to reset the cell + * elements from the tr as they're not attached anymore. Instead + * the cells simply retain the now-unattached elements that were + * assigned on the above iteration. + * + * TODO a cleaner solution, eg. an iterable that only associates + * the elements once + */ + cells = flyweightRow + .getUnattachedCells(offset, numberOfColumns); + getEscalatorUpdater().postDetach(flyweightRow, cells); + + assert flyweightRow.teardown(); + } + reapplyRowWidths(); + + final int firstRemovedColumnLeft = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, offset)); + final boolean columnsWereRemovedFromLeftOfTheViewport = scroller.lastScrollLeft > firstRemovedColumnLeft; + + if (columnsWereRemovedFromLeftOfTheViewport) { + int removedColumnsPxAmount = 0; + for (ColumnConfigurationImpl.Column removedColumn : removedColumns) { + removedColumnsPxAmount += removedColumn + .getCalculatedWidth(); + } + final int leftByDiff = (int) (scroller.lastScrollLeft - removedColumnsPxAmount); + final int newScrollLeft = Math.max(firstRemovedColumnLeft, + leftByDiff); + horizontalScrollbar.setScrollPos(newScrollLeft); + } + + // this needs to be after the scroll position adjustment above. + scroller.recalculateScrollbarsForVirtualViewport(); + + /* + * Because we might remove columns where affected by colspans, it's + * easiest to simply redraw everything when columns are modified. + * + * Yes, this is a TODO [[optimize]]. + */ + if (getRowCount() > 0 + && getColumnConfiguration().getColumnCount() > 0) { + refreshRows(0, getRowCount()); + } + } + + protected void paintInsertColumns(final int offset, + final int numberOfColumns, boolean frozen) { + final NodeList<Node> childNodes = root.getChildNodes(); + + for (int row = 0; row < childNodes.getLength(); row++) { + final TableRowElement tr = getTrByVisualIndex(row); + paintInsertCells(tr, row, offset, numberOfColumns); + } + reapplyRowWidths(); + + if (frozen) { + for (int col = offset; col < offset + numberOfColumns; col++) { + setColumnFrozen(col, true); + } + } + + // this needs to be before the scrollbar adjustment. + scroller.recalculateScrollbarsForVirtualViewport(); + + int pixelsToInsertedColumn = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, offset)); + final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn; + + if (columnsWereAddedToTheLeftOfViewport) { + int insertedColumnsWidth = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(offset, + numberOfColumns)); + horizontalScrollbar.setScrollPos(scroller.lastScrollLeft + + insertedColumnsWidth); + } + + /* + * Because we might insert columns where affected by colspans, it's + * easiest to simply redraw everything when columns are modified. + * + * Yes, this is a TODO [[optimize]]. + */ + if (getRowCount() > 0 + && getColumnConfiguration().getColumnCount() > 1) { + refreshRows(0, getRowCount()); + } + } + + /** + * Inserts new cell elements into a single row element, invoking + * {@link #getEscalatorUpdater()} + * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and + * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before + * and after inserting the cells, respectively. + * <p> + * Precondition: The row must be already attached to the DOM and the + * FlyweightCell instances corresponding to the new columns added to + * {@code flyweightRow}. + * + * @param tr + * the row in which to insert the cells + * @param logicalRowIndex + * the index of the row + * @param offset + * the index of the first cell + * @param numberOfCells + * the number of cells to insert + */ + private void paintInsertCells(final TableRowElement tr, + int logicalRowIndex, final int offset, final int numberOfCells) { + + assert Document.get().isOrHasChild(tr) : "The row must be attached to the document"; + + flyweightRow.setup(tr, logicalRowIndex, + columnConfiguration.getCalculatedColumnWidths()); + + Iterable<FlyweightCell> cells = flyweightRow.getUnattachedCells( + offset, numberOfCells); + + final int rowHeight = getDefaultRowHeight(); + for (FlyweightCell cell : cells) { + final int colWidth = columnConfiguration + .getColumnWidthActual(cell.getColumn()); + final TableCellElement cellElem = createCellElement(rowHeight, + colWidth); + cell.setElement(cellElem); + } + + getEscalatorUpdater().preAttach(flyweightRow, cells); + + Node referenceCell; + if (offset != 0) { + referenceCell = tr.getChild(offset - 1); + } else { + referenceCell = null; + } + for (FlyweightCell cell : cells) { + referenceCell = insertAfterReferenceAndUpdateIt(tr, + cell.getElement(), referenceCell); + } + + getEscalatorUpdater().postAttach(flyweightRow, cells); + + assert flyweightRow.teardown(); + } + + public void setColumnFrozen(int column, boolean frozen) { + final NodeList<TableRowElement> childRows = root.getRows(); + + for (int row = 0; row < childRows.getLength(); row++) { + final TableRowElement tr = childRows.getItem(row); + + TableCellElement cell = tr.getCells().getItem(column); + if (frozen) { + cell.addClassName("frozen"); + } else { + cell.removeClassName("frozen"); + position.reset(cell); + } + } + + if (frozen) { + updateFreezePosition(column, scroller.lastScrollLeft); + } + } + + public void updateFreezePosition(int column, double scrollLeft) { + final NodeList<TableRowElement> childRows = root.getRows(); + + for (int row = 0; row < childRows.getLength(); row++) { + final TableRowElement tr = childRows.getItem(row); + + TableCellElement cell = tr.getCells().getItem(column); + position.set(cell, scrollLeft, 0); + } + } + + /** + * Iterates through all the cells in a column and returns the width of + * the widest element in this RowContainer. + * + * @param index + * the index of the column to inspect + * @return the pixel width of the widest element in the indicated column + */ + public int calculateMaxColWidth(int index) { + TableRowElement row = TableRowElement.as(root + .getFirstChildElement()); + int maxWidth = 0; + while (row != null) { + final TableCellElement cell = row.getCells().getItem(index); + final boolean isVisible = !cell.getStyle().getDisplay() + .equals(Display.NONE.getCssName()); + if (isVisible) { + maxWidth = Math.max(maxWidth, cell.getScrollWidth()); + } + row = TableRowElement.as(row.getNextSiblingElement()); + } + return maxWidth; + } + + /** + * Reapplies all the cells' widths according to the calculated widths in + * the column configuration. + */ + public void reapplyColumnWidths() { + Element row = root.getFirstChildElement(); + while (row != null) { + Element cell = row.getFirstChildElement(); + int columnIndex = 0; + while (cell != null) { + @SuppressWarnings("hiding") + final int width = getCalculatedColumnWidthWithColspan(cell, + columnIndex); + + /* + * TODO Should Escalator implement ProvidesResize at some + * point, this is where we need to do that. + */ + cell.getStyle().setWidth(width, Unit.PX); + + cell = cell.getNextSiblingElement(); + columnIndex++; + } + row = row.getNextSiblingElement(); + } + + reapplyRowWidths(); + } + + private int getCalculatedColumnWidthWithColspan(final Element cell, + final int columnIndex) { + final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR); + Range spannedColumns = Range.withLength(columnIndex, colspan); + + /* + * Since browsers don't explode with overflowing colspans, escalator + * shouldn't either. + */ + if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) { + spannedColumns = Range.between(columnIndex, + columnConfiguration.getColumnCount()); + } + return columnConfiguration + .getCalculatedColumnsWidth(spannedColumns); + } + + /** + * Applies the total length of the columns to each row element. + * <p> + * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this + * method only modifies the width of the {@code <tr>} element, not the + * cells within. + */ + protected void reapplyRowWidths() { + int rowWidth = columnConfiguration.calculateRowWidth(); + + com.google.gwt.dom.client.Element row = root.getFirstChildElement(); + while (row != null) { + row.getStyle().setWidth(rowWidth, Unit.PX); + row = row.getNextSiblingElement(); + } + } + + /** + * The primary style name for the container. + * + * @param primaryStyleName + * the style name to use as prefix for all row and cell style + * names. + */ + protected void setStylePrimaryName(String primaryStyleName) { + String oldStyle = getStylePrimaryName(); + if (SharedUtil.equals(oldStyle, primaryStyleName)) { + return; + } + + this.primaryStyleName = primaryStyleName; + + // Update already rendered rows and cells + TableRowElement row = root.getRows().getItem(0); + while (row != null) { + UIObject.setStylePrimaryName(row, primaryStyleName + "-row"); + TableCellElement cell = row.getCells().getItem(0); + while (cell != null) { + UIObject.setStylePrimaryName(cell, primaryStyleName + + "-cell"); + cell = TableCellElement.as(cell.getNextSiblingElement()); + } + row = TableRowElement.as(row.getNextSiblingElement()); + } + } + + /** + * Returns the primary style name of the container. + * + * @return The primary style name or <code>null</code> if not set. + */ + protected String getStylePrimaryName() { + return primaryStyleName; + } + + @Override + public void setDefaultRowHeight(int px) throws IllegalArgumentException { + if (px < 1) { + throw new IllegalArgumentException("Height must be positive. " + + px + " was given."); + } + + defaultRowHeightShouldBeAutodetected = false; + defaultRowHeight = px; + reapplyDefaultRowHeights(); + } + + @Override + public int getDefaultRowHeight() { + return defaultRowHeight; + } + + /** + * The default height of rows has (most probably) changed. + * <p> + * Make sure that the displayed rows with a default height are updated + * in height and top position. + * <p> + * <em>Note:</em>This implementation should not call + * {@link Escalator#recalculateElementSizes()} - it is done by the + * discretion of the caller of this method. + */ + protected abstract void reapplyDefaultRowHeights(); + + protected void reapplyRowHeight(final TableRowElement tr, + final int heightPx) { + Element cellElem = tr.getFirstChildElement(); + while (cellElem != null) { + cellElem.getStyle().setHeight(heightPx, Unit.PX); + cellElem = cellElem.getNextSiblingElement(); + } + + /* + * no need to apply height to tr-element, it'll be resized + * implicitly. + */ + } + + @SuppressWarnings("boxing") + protected void setRowPosition(final TableRowElement tr, final int x, + final int y) { + position.set(tr, x, y); + rowTopPositionMap.put(tr, y); + } + + @SuppressWarnings("boxing") + protected int getRowTop(final TableRowElement tr) { + return rowTopPositionMap.get(tr); + } + + protected void removeRowPosition(TableRowElement tr) { + rowTopPositionMap.remove(tr); + } + + public void autodetectRowHeight() { + Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { + + @Override + public void execute() { + if (defaultRowHeightShouldBeAutodetected && isAttached()) { + final Element detectionTr = DOM.createTR(); + detectionTr + .setClassName(getStylePrimaryName() + "-row"); + + final Element cellElem = DOM + .createElement(getCellElementTagName()); + cellElem.setClassName(getStylePrimaryName() + "-cell"); + cellElem.setInnerHTML("foo"); + + detectionTr.appendChild(cellElem); + root.appendChild(detectionTr); + defaultRowHeight = Math.max(1, + cellElem.getOffsetHeight()); + root.removeChild(detectionTr); + + if (root.hasChildNodes()) { + reapplyDefaultRowHeights(); + } + + defaultRowHeightShouldBeAutodetected = false; + } + } + }); + } + + @Override + public Cell getCell(final Element element) { + if (element == null) { + throw new IllegalArgumentException("Element cannot be null"); + } + + /* + * Ensure that element is not root nor the direct descendant of root + * (a row) and ensure the element is inside the dom hierarchy of the + * root element. If not, return. + */ + if (root == element || element.getParentElement() == root + || !root.isOrHasChild(element)) { + return null; + } + + /* + * Ensure element is the cell element by iterating up the DOM + * hierarchy until reaching cell element. + */ + Element cellElementCandidate = element; + while (cellElementCandidate.getParentElement().getParentElement() != root) { + cellElementCandidate = cellElementCandidate.getParentElement(); + } + final TableCellElement cellElement = TableCellElement + .as(cellElementCandidate); + + // Find dom column + int domColumnIndex = -1; + for (Element e = cellElement; e != null; e = e + .getPreviousSiblingElement()) { + domColumnIndex++; + } + + // Find dom row + int domRowIndex = -1; + for (Element e = cellElement.getParentElement(); e != null; e = e + .getPreviousSiblingElement()) { + domRowIndex++; + } + + return new Cell(domRowIndex, domColumnIndex, cellElement); + } + } + + private abstract class AbstractStaticRowContainer extends + AbstractRowContainer { + public AbstractStaticRowContainer(final TableSectionElement headElement) { + super(headElement); + } + + @Override + protected void paintRemoveRows(final int index, final int numberOfRows) { + for (int i = index; i < index + numberOfRows; i++) { + final TableRowElement tr = root.getRows().getItem(index); + paintRemoveRow(tr, index); + } + recalculateSectionHeight(); + } + + @Override + protected TableRowElement getTrByVisualIndex(final int index) + throws IndexOutOfBoundsException { + if (index >= 0 && index < root.getChildCount()) { + return root.getRows().getItem(index); + } else { + throw new IndexOutOfBoundsException("No such visual index: " + + index); + } + } + + @Override + public void insertRows(int index, int numberOfRows) { + super.insertRows(index, numberOfRows); + recalculateElementSizes(); + applyHeightByRows(); + } + + @Override + public void removeRows(int index, int numberOfRows) { + super.removeRows(index, numberOfRows); + recalculateElementSizes(); + applyHeightByRows(); + } + + @Override + protected void reapplyDefaultRowHeights() { + if (root.getChildCount() == 0) { + return; + } + + Profiler.enter("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights"); + + TableRowElement tr = root.getRows().getItem(0); + while (tr != null) { + reapplyRowHeight(tr, getDefaultRowHeight()); + tr = TableRowElement.as(tr.getNextSiblingElement()); + } + + /* + * Because all rows are immediately displayed in the static row + * containers, the section's overall height has most probably + * changed. + */ + recalculateSectionHeight(); + + Profiler.leave("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights"); + } + + @Override + protected void recalculateSectionHeight() { + Profiler.enter("Escalator.AbstractStaticRowContainer.recalculateSectionHeight"); + + int newHeight = calculateEstimatedTotalRowHeight(); + if (newHeight != heightOfSection) { + heightOfSection = newHeight; + sectionHeightCalculated(); + body.verifyEscalatorCount(); + } + + Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight"); + } + + /** + * Informs the row container that the height of its respective table + * section has changed. + * <p> + * These calculations might affect some layouting logic, such as the + * body is being offset by the footer, the footer needs to be readjusted + * according to its height, and so on. + * <p> + * A table section is either header, body or footer. + */ + protected abstract void sectionHeightCalculated(); + } + + private class HeaderRowContainer extends AbstractStaticRowContainer { + public HeaderRowContainer(final TableSectionElement headElement) { + super(headElement); + } + + @Override + protected void sectionHeightCalculated() { + bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX); + verticalScrollbar.getElement().getStyle() + .setTop(heightOfSection, Unit.PX); + } + + @Override + protected String getCellElementTagName() { + return "th"; + } + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + UIObject.setStylePrimaryName(root, primaryStyleName + "-header"); + } + } + + private class FooterRowContainer extends AbstractStaticRowContainer { + public FooterRowContainer(final TableSectionElement footElement) { + super(footElement); + } + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + UIObject.setStylePrimaryName(root, primaryStyleName + "-footer"); + } + + @Override + protected String getCellElementTagName() { + return "td"; + } + + @Override + protected void sectionHeightCalculated() { + int vscrollHeight = (int) Math.floor(heightOfEscalator + - header.heightOfSection - footer.heightOfSection); + + final boolean horizontalScrollbarNeeded = columnConfiguration + .calculateRowWidth() > widthOfEscalator; + if (horizontalScrollbarNeeded) { + vscrollHeight -= horizontalScrollbar.getScrollbarThickness(); + } + + verticalScrollbar.setOffsetSize(vscrollHeight); + } + } + + private class BodyRowContainer extends AbstractRowContainer { + /* + * TODO [[optimize]]: check whether a native JsArray might be faster + * than LinkedList + */ + /** + * The order in which row elements are rendered visually in the browser, + * with the help of CSS tricks. Usually has nothing to do with the DOM + * order. + * + * @see #sortDomElements() + */ + private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<TableRowElement>(); + + /** + * The logical index of the topmost row. + * + * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)}, + * {@link #updateTopRowLogicalIndex(int)} and + * {@link #getTopRowLogicalIndex()} instead + */ + @Deprecated + private int topRowLogicalIndex = 0; + + private void setTopRowLogicalIndex(int topRowLogicalIndex) { + if (LogConfiguration.loggingIsEnabled(Level.INFO)) { + Logger.getLogger("Escalator.BodyRowContainer").fine( + "topRowLogicalIndex: " + this.topRowLogicalIndex + + " -> " + topRowLogicalIndex); + } + assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative"; + /* + * if there's a smart way of evaluating and asserting the max index, + * this would be a nice place to put it. I haven't found out an + * effective and generic solution. + */ + + this.topRowLogicalIndex = topRowLogicalIndex; + } + + private int getTopRowLogicalIndex() { + return topRowLogicalIndex; + } + + private void updateTopRowLogicalIndex(int diff) { + setTopRowLogicalIndex(topRowLogicalIndex + diff); + } + + private class DeferredDomSorter { + private static final int SORT_DELAY_MILLIS = 50; + + // as it happens, 3 frames = 50ms @ 60fps. + private static final int REQUIRED_FRAMES_PASSED = 3; + + private final AnimationCallback frameCounter = new AnimationCallback() { + @Override + public void execute(double timestamp) { + framesPassed++; + boolean domWasSorted = sortIfConditionsMet(); + if (!domWasSorted) { + animationHandle = AnimationScheduler.get() + .requestAnimationFrame(this); + } + } + }; + + private int framesPassed; + private double startTime; + private AnimationHandle animationHandle; + + public void reschedule() { + resetConditions(); + animationHandle = AnimationScheduler.get() + .requestAnimationFrame(frameCounter); + } + + private boolean sortIfConditionsMet() { + boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED; + boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS; + boolean conditionsMet = enoughFramesHavePassed + && enoughTimeHasPassed; + + if (conditionsMet) { + resetConditions(); + sortDomElements(); + } + + return conditionsMet; + } + + private void resetConditions() { + if (animationHandle != null) { + animationHandle.cancel(); + animationHandle = null; + } + startTime = Duration.currentTimeMillis(); + framesPassed = 0; + } + } + + private DeferredDomSorter domSorter = new DeferredDomSorter(); + + public BodyRowContainer(final TableSectionElement bodyElement) { + super(bodyElement); + } + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); + } + + public void updateEscalatorRowsOnScroll() { + if (visualRowOrder.isEmpty()) { + return; + } + + boolean rowsWereMoved = false; + + final double topRowPos = getRowTop(visualRowOrder.getFirst()); + // TODO [[mpixscroll]] + final double scrollTop = tBodyScrollTop; + final double viewportOffset = topRowPos - scrollTop; + + /* + * TODO [[optimize]] this if-else can most probably be refactored + * into a neater block of code + */ + + if (viewportOffset > 0) { + // there's empty room on top + + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + int originalRowsToMove = (int) Math.ceil(viewportOffset + / getDefaultRowHeight()); + int rowsToMove = Math.min(originalRowsToMove, + root.getChildCount()); + + final int end = root.getChildCount(); + final int start = end - rowsToMove; + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + moveAndUpdateEscalatorRows(Range.between(start, end), 0, + logicalRowIndex); + + updateTopRowLogicalIndex(-originalRowsToMove); + + rowsWereMoved = true; + } + + else if (viewportOffset + getDefaultRowHeight() <= 0) { + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + + /* + * the viewport has been scrolled more than the topmost visual + * row. + */ + + int originalRowsToMove = (int) Math.abs(viewportOffset + / getDefaultRowHeight()); + int rowsToMove = Math.min(originalRowsToMove, + root.getChildCount()); + + int logicalRowIndex; + if (rowsToMove < root.getChildCount()) { + /* + * We scroll so little that we can just keep adding the rows + * below the current escalator + */ + logicalRowIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + 1; + } else { + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + /* + * Since we're moving all escalator rows, we need to + * calculate the first logical row index from the scroll + * position. + */ + logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + } + + /* + * Since we're moving the viewport downwards, the visual index + * is always at the bottom. Note: Due to how + * moveAndUpdateEscalatorRows works, this will work out even if + * we move all the rows, and try to place them "at the end". + */ + final int targetVisualIndex = root.getChildCount(); + + // make sure that we don't move rows over the data boundary + boolean aRowWasLeftBehind = false; + if (logicalRowIndex + rowsToMove > getRowCount()) { + /* + * TODO [[rowheight]]: with constant row heights, there's + * always exactly one row that will be moved beyond the data + * source, when viewport is scrolled to the end. This, + * however, isn't guaranteed anymore once row heights start + * varying. + */ + rowsToMove--; + aRowWasLeftBehind = true; + } + + moveAndUpdateEscalatorRows(Range.between(0, rowsToMove), + targetVisualIndex, logicalRowIndex); + + if (aRowWasLeftBehind) { + /* + * To keep visualRowOrder as a spatially contiguous block of + * rows, let's make sure that the one row we didn't move + * visually still stays with the pack. + */ + final Range strayRow = Range.withOnly(0); + + /* + * We cannot trust getLogicalRowIndex, because it hasn't yet + * been updated. But since we're leaving rows behind, it + * means we've scrolled to the bottom. So, instead, we + * simply count backwards from the end. + */ + final int topLogicalIndex = getRowCount() + - visualRowOrder.size(); + moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); + } + + final int naiveNewLogicalIndex = getTopRowLogicalIndex() + + originalRowsToMove; + final int maxLogicalIndex = getRowCount() + - visualRowOrder.size(); + setTopRowLogicalIndex(Math.min(naiveNewLogicalIndex, + maxLogicalIndex)); + + rowsWereMoved = true; + } + + if (rowsWereMoved) { + fireRowVisibilityChangeEvent(); + + if (scroller.touchHandlerBundle.touches == 0) { + /* + * this will never be called on touch scrolling. That is + * handled separately and explicitly by + * TouchHandlerBundle.touchEnd(); + */ + domSorter.reschedule(); + } + } + } + + @Override + protected List<TableRowElement> paintInsertRows(final int index, + final int numberOfRows) { + if (numberOfRows == 0) { + return Collections.emptyList(); + } + + /* + * TODO: this method should probably only add physical rows, and not + * populate them - let everything be populated as appropriate by the + * logic that follows. + * + * This also would lead to the fact that paintInsertRows wouldn't + * need to return anything. + */ + final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded( + index, numberOfRows); + + /* + * insertRows will always change the number of rows - update the + * scrollbar sizes. + */ + scroller.recalculateScrollbarsForVirtualViewport(); + + /* + * FIXME [[rowheight]]: coded to work only with default row heights + * - will not work with variable row heights + */ + final boolean addedRowsAboveCurrentViewport = index + * getDefaultRowHeight() < getScrollTop(); + final boolean addedRowsBelowCurrentViewport = index + * getDefaultRowHeight() > getScrollTop() + + calculateHeight(); + + if (addedRowsAboveCurrentViewport) { + /* + * We need to tweak the virtual viewport (scroll handle + * positions, table "scroll position" and row locations), but + * without re-evaluating any rows. + */ + + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + final int yDelta = numberOfRows * getDefaultRowHeight(); + adjustScrollPosIgnoreEvents(yDelta); + updateTopRowLogicalIndex(numberOfRows); + } + + else if (addedRowsBelowCurrentViewport) { + // NOOP, we already recalculated scrollbars. + } + + else { // some rows were added inside the current viewport + + final int unupdatedLogicalStart = index + addedRows.size(); + final int visualOffset = getLogicalRowIndex(visualRowOrder + .getFirst()); + + /* + * At this point, we have added new escalator rows, if so + * needed. + * + * If more rows were added than the new escalator rows can + * account for, we need to start to spin the escalator to update + * the remaining rows aswell. + */ + final int rowsStillNeeded = numberOfRows - addedRows.size(); + final Range unupdatedVisual = convertToVisual(Range.withLength( + unupdatedLogicalStart, rowsStillNeeded)); + final int end = root.getChildCount(); + final int start = end - unupdatedVisual.length(); + final int visualTargetIndex = unupdatedLogicalStart + - visualOffset; + moveAndUpdateEscalatorRows(Range.between(start, end), + visualTargetIndex, unupdatedLogicalStart); + + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + // move the surrounding rows to their correct places. + int rowTop = (unupdatedLogicalStart + (end - start)) + * getDefaultRowHeight(); + final ListIterator<TableRowElement> i = visualRowOrder + .listIterator(visualTargetIndex + (end - start)); + while (i.hasNext()) { + final TableRowElement tr = i.next(); + setRowPosition(tr, 0, rowTop); + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + rowTop += getDefaultRowHeight(); + } + + fireRowVisibilityChangeEvent(); + sortDomElements(); + } + return addedRows; + } + + /** + * Move escalator rows around, and make sure everything gets + * appropriately repositioned and repainted. + * + * @param visualSourceRange + * the range of rows to move to a new place + * @param visualTargetIndex + * the visual index where the rows will be placed to + * @param logicalTargetIndex + * the logical index to be assigned to the first moved row + * @throws IllegalArgumentException + * if any of <code>visualSourceRange.getStart()</code>, + * <code>visualTargetIndex</code> or + * <code>logicalTargetIndex</code> is a negative number; or + * if <code>visualTargetInfo</code> is greater than the + * number of escalator rows. + */ + private void moveAndUpdateEscalatorRows(final Range visualSourceRange, + final int visualTargetIndex, final int logicalTargetIndex) + throws IllegalArgumentException { + + if (visualSourceRange.isEmpty()) { + return; + } + + if (visualSourceRange.getStart() < 0) { + throw new IllegalArgumentException( + "Logical source start must be 0 or greater (was " + + visualSourceRange.getStart() + ")"); + } else if (logicalTargetIndex < 0) { + throw new IllegalArgumentException( + "Logical target must be 0 or greater"); + } else if (visualTargetIndex < 0) { + throw new IllegalArgumentException( + "Visual target must be 0 or greater"); + } else if (visualTargetIndex > root.getChildCount()) { + throw new IllegalArgumentException( + "Visual target must not be greater than the number of escalator rows"); + } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) { + final int logicalEndIndex = logicalTargetIndex + + visualSourceRange.length() - 1; + throw new IllegalArgumentException( + "Logical target leads to rows outside of the data range (" + + logicalTargetIndex + ".." + logicalEndIndex + + ")"); + } + + /* + * Since we move a range into another range, the indices might move + * about. Having 10 rows, if we move 0..1 to index 10 (to the end of + * the collection), the target range will end up being 8..9, instead + * of 10..11. + * + * This applies only if we move elements forward in the collection, + * not backward. + */ + final int adjustedVisualTargetIndex; + if (visualSourceRange.getStart() < visualTargetIndex) { + adjustedVisualTargetIndex = visualTargetIndex + - visualSourceRange.length(); + } else { + adjustedVisualTargetIndex = visualTargetIndex; + } + + if (visualSourceRange.getStart() != adjustedVisualTargetIndex) { + + /* + * Reorder the rows to their correct places within + * visualRowOrder (unless rows are moved back to their original + * places) + */ + + /* + * TODO [[optimize]]: move whichever set is smaller: the ones + * explicitly moved, or the others. So, with 10 escalator rows, + * if we are asked to move idx[0..8] to the end of the list, + * it's faster to just move idx[9] to the beginning. + */ + + final List<TableRowElement> removedRows = new ArrayList<TableRowElement>( + visualSourceRange.length()); + for (int i = 0; i < visualSourceRange.length(); i++) { + final TableRowElement tr = visualRowOrder + .remove(visualSourceRange.getStart()); + removedRows.add(tr); + } + visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows); + } + + { // Refresh the contents of the affected rows + final ListIterator<TableRowElement> iter = visualRowOrder + .listIterator(adjustedVisualTargetIndex); + for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex + + visualSourceRange.length(); logicalIndex++) { + final TableRowElement tr = iter.next(); + refreshRow(tr, logicalIndex); + } + } + + { // Reposition the rows that were moved + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + int newRowTop = logicalTargetIndex * getDefaultRowHeight(); + + final ListIterator<TableRowElement> iter = visualRowOrder + .listIterator(adjustedVisualTargetIndex); + for (int i = 0; i < visualSourceRange.length(); i++) { + final TableRowElement tr = iter.next(); + setRowPosition(tr, 0, newRowTop); + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + newRowTop += getDefaultRowHeight(); + } + } + } + + /** + * Adjust the scroll position without having the scroll handler have any + * side-effects. + * <p> + * <em>Note:</em> {@link Scroller#onScroll()} <em>will</em> be + * triggered, but it will not do anything, with the help of + * {@link Escalator#internalScrollEventCalls}. + * + * @param yDelta + * the delta of pixels to scrolls. A positive value moves the + * viewport downwards, while a negative value moves the + * viewport upwards + */ + public void adjustScrollPosIgnoreEvents(final double yDelta) { + if (yDelta == 0) { + return; + } + + internalScrollEventCalls++; + verticalScrollbar.setScrollPosByDelta(yDelta); + + /* + * FIXME [[rowheight]]: coded to work only with default row heights + * - will not work with variable row heights + */ + final int rowTopPos = (int) yDelta + - ((int) yDelta % getDefaultRowHeight()); + for (final TableRowElement tr : visualRowOrder) { + setRowPosition(tr, 0, getRowTop(tr) + rowTopPos); + } + setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta); + } + + /** + * Adds new physical escalator rows to the DOM at the given index if + * there's still a need for more escalator rows. + * <p> + * If Escalator already is at (or beyond) max capacity, this method does + * nothing to the DOM. + * + * @param index + * the index at which to add new escalator rows. + * <em>Note:</em>It is assumed that the index is both the + * visual index and the logical index. + * @param numberOfRows + * the number of rows to add at <code>index</code> + * @return a list of the added rows + */ + private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded( + final int index, final int numberOfRows) { + + final int escalatorRowsStillFit = getMaxEscalatorRowCapacity() + - root.getChildCount(); + final int escalatorRowsNeeded = Math.min(numberOfRows, + escalatorRowsStillFit); + + if (escalatorRowsNeeded > 0) { + + final List<TableRowElement> addedRows = super.paintInsertRows( + index, escalatorRowsNeeded); + visualRowOrder.addAll(index, addedRows); + + /* + * We need to figure out the top positions for the rows we just + * added. + */ + for (int i = 0; i < addedRows.size(); i++) { + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + setRowPosition(addedRows.get(i), 0, (index + i) + * getDefaultRowHeight()); + } + + /* Move the other rows away from above the added escalator rows */ + for (int i = index + addedRows.size(); i < visualRowOrder + .size(); i++) { + final TableRowElement tr = visualRowOrder.get(i); + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + setRowPosition(tr, 0, i * getDefaultRowHeight()); + } + + return addedRows; + } else { + return new ArrayList<TableRowElement>(); + } + } + + private int getMaxEscalatorRowCapacity() { + /* + * FIXME [[rowheight]]: coded to work only with default row heights + * - will not work with variable row heights + */ + final int maxEscalatorRowCapacity = (int) Math + .ceil(calculateHeight() / getDefaultRowHeight()) + 1; + + /* + * maxEscalatorRowCapacity can become negative if the headers and + * footers start to overlap. This is a crazy situation, but Vaadin + * blinks the components a lot, so it's feasible. + */ + return Math.max(0, maxEscalatorRowCapacity); + } + + @Override + protected void paintRemoveRows(final int index, final int numberOfRows) { + if (numberOfRows == 0) { + return; + } + + final Range viewportRange = Range.withLength( + getLogicalRowIndex(visualRowOrder.getFirst()), + visualRowOrder.size()); + + final Range removedRowsRange = Range + .withLength(index, numberOfRows); + + final Range[] partitions = removedRowsRange + .partitionWith(viewportRange); + final Range removedAbove = partitions[0]; + final Range removedLogicalInside = partitions[1]; + final Range removedVisualInside = convertToVisual(removedLogicalInside); + + /* + * TODO: extract the following if-block to a separate method. I'll + * leave this be inlined for now, to make linediff-based code + * reviewing easier. Probably will be moved in the following patch + * set. + */ + + /* + * Adjust scroll position in one of two scenarios: + * + * 1) Rows were removed above. Then we just need to adjust the + * scrollbar by the height of the removed rows. + * + * 2) There are no logical rows above, and at least the first (if + * not more) visual row is removed. Then we need to snap the scroll + * position to the first visible row (i.e. reset scroll position to + * absolute 0) + * + * The logic is optimized in such a way that the + * adjustScrollPosIgnoreEvents is called only once, to avoid extra + * reflows, and thus the code might seem a bit obscure. + */ + final boolean firstVisualRowIsRemoved = !removedVisualInside + .isEmpty() && removedVisualInside.getStart() == 0; + + if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + final int yDelta = removedAbove.length() + * getDefaultRowHeight(); + final int firstLogicalRowHeight = getDefaultRowHeight(); + final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar + .getScrollPos() - yDelta < firstLogicalRowHeight; + + if (removedVisualInside.isEmpty() + && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) { + /* + * rows were removed from above the viewport, so all we need + * to do is to adjust the scroll position to account for the + * removed rows + */ + adjustScrollPosIgnoreEvents(-yDelta); + } else if (removalScrollsToShowFirstLogicalRow) { + /* + * It seems like we've removed all rows from above, and also + * into the current viewport. This means we'll need to even + * out the scroll position to exactly 0 (i.e. adjust by the + * current negative scrolltop, presto!), so that it isn't + * aligned funnily + */ + adjustScrollPosIgnoreEvents(-verticalScrollbar + .getScrollPos()); + } + } + + // ranges evaluated, let's do things. + if (!removedVisualInside.isEmpty()) { + int escalatorRowCount = bodyElem.getChildCount(); + + /* + * If we're left with less rows than the number of escalators, + * remove the unused ones. + */ + final int escalatorRowsToRemove = escalatorRowCount + - getRowCount(); + if (escalatorRowsToRemove > 0) { + for (int i = 0; i < escalatorRowsToRemove; i++) { + final TableRowElement tr = visualRowOrder + .remove(removedVisualInside.getStart()); + + paintRemoveRow(tr, index); + removeRowPosition(tr); + } + escalatorRowCount -= escalatorRowsToRemove; + + /* + * Because we're removing escalator rows, we don't have + * anything to scroll by. Let's make sure the viewport is + * scrolled to top, to render any rows possibly left above. + */ + body.setBodyScrollPosition(tBodyScrollLeft, 0); + + /* + * We might have removed some rows from the middle, so let's + * make sure we're not left with any holes. Also remember: + * visualIndex == logicalIndex applies now. + */ + final int dirtyRowsStart = removedLogicalInside.getStart(); + for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { + final TableRowElement tr = visualRowOrder.get(i); + /* + * FIXME [[rowheight]]: coded to work only with default + * row heights - will not work with variable row heights + */ + setRowPosition(tr, 0, i * getDefaultRowHeight()); + } + + /* + * this is how many rows appeared into the viewport from + * below + */ + final int rowsToUpdateDataOn = numberOfRows + - escalatorRowsToRemove; + final int start = Math.max(0, escalatorRowCount + - rowsToUpdateDataOn); + final int end = escalatorRowCount; + for (int i = start; i < end; i++) { + final TableRowElement tr = visualRowOrder.get(i); + refreshRow(tr, i); + } + } + + else { + // No escalator rows need to be removed. + + /* + * Two things (or a combination thereof) can happen: + * + * 1) We're scrolled to the bottom, the last rows are + * removed. SOLUTION: moveAndUpdateEscalatorRows the + * bottommost rows, and place them at the top to be + * refreshed. + * + * 2) We're scrolled somewhere in the middle, arbitrary rows + * are removed. SOLUTION: moveAndUpdateEscalatorRows the + * removed rows, and place them at the bottom to be + * refreshed. + * + * Since a combination can also happen, we need to handle + * this in a smart way, all while avoiding + * double-refreshing. + */ + + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + final int contentBottom = getRowCount() + * getDefaultRowHeight(); + final int viewportBottom = (int) (tBodyScrollTop + calculateHeight()); + if (viewportBottom <= contentBottom) { + /* + * We're in the middle of the row container, everything + * is added to the bottom + */ + paintRemoveRowsAtMiddle(removedLogicalInside, + removedVisualInside, 0); + } + + else if (contentBottom + + (numberOfRows * getDefaultRowHeight()) + - viewportBottom < getDefaultRowHeight()) { + /* + * FIXME [[rowheight]]: above coded to work only with + * default row heights - will not work with variable row + * heights + */ + + /* + * We're at the end of the row container, everything is + * added to the top. + */ + paintRemoveRowsAtBottom(removedLogicalInside, + removedVisualInside); + updateTopRowLogicalIndex(-removedLogicalInside.length()); + } + + else { + /* + * We're in a combination, where we need to both scroll + * up AND show new rows at the bottom. + * + * Example: Scrolled down to show the second to last + * row. Remove two. Viewport scrolls up, revealing the + * row above row. The last element collapses up and into + * view. + * + * Reminder: this use case handles only the case when + * there are enough escalator rows to still render a + * full view. I.e. all escalator rows will _always_ be + * populated + */ + /*- + * 1 1 |1| <- newly rendered + * |2| |2| |2| + * |3| ==> |*| ==> |5| <- newly rendered + * |4| |*| + * 5 5 + * + * 1 1 |1| <- newly rendered + * |2| |*| |4| + * |3| ==> |*| ==> |5| <- newly rendered + * |4| |4| + * 5 5 + */ + + /* + * STEP 1: + * + * reorganize deprecated escalator rows to bottom, but + * don't re-render anything yet + */ + /*- + * 1 1 1 + * |2| |*| |4| + * |3| ==> |*| ==> |*| + * |4| |4| |*| + * 5 5 5 + */ + double newTop = getRowTop(visualRowOrder + .get(removedVisualInside.getStart())); + for (int i = 0; i < removedVisualInside.length(); i++) { + final TableRowElement tr = visualRowOrder + .remove(removedVisualInside.getStart()); + visualRowOrder.addLast(tr); + } + + for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) { + final TableRowElement tr = visualRowOrder.get(i); + setRowPosition(tr, 0, (int) newTop); + + /* + * FIXME [[rowheight]]: coded to work only with + * default row heights - will not work with variable + * row heights + */ + newTop += getDefaultRowHeight(); + } + + /* + * STEP 2: + * + * manually scroll + */ + /*- + * 1 |1| <-- newly rendered (by scrolling) + * |4| |4| + * |*| ==> |*| + * |*| + * 5 5 + */ + final double newScrollTop = contentBottom + - calculateHeight(); + setScrollTop(newScrollTop); + /* + * Manually call the scroll handler, so we get immediate + * effects in the escalator. + */ + scroller.onScroll(); + internalScrollEventCalls++; + + /* + * Move the bottommost (n+1:th) escalator row to top, + * because scrolling up doesn't handle that for us + * automatically + */ + moveAndUpdateEscalatorRows( + Range.withOnly(escalatorRowCount - 1), + 0, + getLogicalRowIndex(visualRowOrder.getFirst()) - 1); + updateTopRowLogicalIndex(-1); + + /* + * STEP 3: + * + * update remaining escalator rows + */ + /*- + * |1| |1| + * |4| ==> |4| + * |*| |5| <-- newly rendered + * + * 5 + */ + + /* + * FIXME [[rowheight]]: coded to work only with default + * row heights - will not work with variable row heights + */ + final int rowsScrolled = (int) (Math + .ceil((viewportBottom - (double) contentBottom) + / getDefaultRowHeight())); + final int start = escalatorRowCount + - (removedVisualInside.length() - rowsScrolled); + final Range visualRefreshRange = Range.between(start, + escalatorRowCount); + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getFirst()) + start; + // in-place move simply re-renders the rows. + moveAndUpdateEscalatorRows(visualRefreshRange, start, + logicalTargetIndex); + } + } + + fireRowVisibilityChangeEvent(); + sortDomElements(); + } + + updateTopRowLogicalIndex(-removedAbove.length()); + + /* + * this needs to be done after the escalator has been shrunk down, + * or it won't work correctly (due to setScrollTop invocation) + */ + scroller.recalculateScrollbarsForVirtualViewport(); + } + + private void paintRemoveRowsAtMiddle(final Range removedLogicalInside, + final Range removedVisualInside, final int logicalOffset) { + /*- + * : : : + * |2| |2| |2| + * |3| ==> |*| ==> |4| + * |4| |4| |6| <- newly rendered + * : : : + */ + + final int escalatorRowCount = visualRowOrder.size(); + + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + - (removedVisualInside.length() - 1) + + logicalOffset; + moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount, + logicalTargetIndex); + + // move the surrounding rows to their correct places. + final ListIterator<TableRowElement> iterator = visualRowOrder + .listIterator(removedVisualInside.getStart()); + + /* + * FIXME [[rowheight]]: coded to work only with default row heights + * - will not work with variable row heights + */ + int rowTop = (removedLogicalInside.getStart() + logicalOffset) + * getDefaultRowHeight(); + for (int i = removedVisualInside.getStart(); i < escalatorRowCount + - removedVisualInside.length(); i++) { + final TableRowElement tr = iterator.next(); + setRowPosition(tr, 0, rowTop); + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + rowTop += getDefaultRowHeight(); + } + } + + private void paintRemoveRowsAtBottom(final Range removedLogicalInside, + final Range removedVisualInside) { + /*- + * : + * : : |4| <- newly rendered + * |5| |5| |5| + * |6| ==> |*| ==> |7| + * |7| |7| + */ + + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getFirst()) - removedVisualInside.length(); + moveAndUpdateEscalatorRows(removedVisualInside, 0, + logicalTargetIndex); + + // move the surrounding rows to their correct places. + final ListIterator<TableRowElement> iterator = visualRowOrder + .listIterator(removedVisualInside.getEnd()); + /* + * FIXME [[rowheight]]: coded to work only with default row heights + * - will not work with variable row heights + */ + int rowTop = removedLogicalInside.getStart() + * getDefaultRowHeight(); + while (iterator.hasNext()) { + final TableRowElement tr = iterator.next(); + setRowPosition(tr, 0, rowTop); + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + rowTop += getDefaultRowHeight(); + } + } + + private int getLogicalRowIndex(final Element tr) { + assert tr.getParentNode() == root : "The given element isn't a row element in the body"; + int internalIndex = visualRowOrder.indexOf(tr); + return getTopRowLogicalIndex() + internalIndex; + } + + @Override + protected void recalculateSectionHeight() { + // NOOP for body, since it doesn't make any sense. + } + + /** + * Adjusts the row index and number to be relevant for the current + * virtual viewport. + * <p> + * It converts a logical range of rows index to the matching visual + * range, truncating the resulting range with the viewport. + * <p> + * <ul> + * <li>Escalator contains logical rows 0..100 + * <li>Current viewport showing logical rows 20..29 + * <li>convertToVisual([20..29]) → [0..9] + * <li>convertToVisual([15..24]) → [0..4] + * <li>convertToVisual([25..29]) → [5..9] + * <li>convertToVisual([26..39]) → [6..9] + * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([0..100]) → [0..9] + * </ul> + * + * @return a logical range converted to a visual range, truncated to the + * current viewport. The first visual row has the index 0. + */ + private Range convertToVisual(final Range logicalRange) { + if (logicalRange.isEmpty()) { + return logicalRange; + } else if (visualRowOrder.isEmpty()) { + // empty range + return Range.withLength(0, 0); + } + + /* + * TODO [[rowheight]]: these assumptions will be totally broken with + * variable row heights. + */ + final int maxEscalatorRows = getMaxEscalatorRowCapacity(); + final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder + .getFirst()); + + final Range[] partitions = logicalRange.partitionWith(Range + .withLength(currentTopRowIndex, maxEscalatorRows)); + final Range insideRange = partitions[1]; + return insideRange.offsetBy(-currentTopRowIndex); + } + + @Override + protected String getCellElementTagName() { + return "td"; + } + + /** + * Calculates the height of the {@code <tbody>} as it should be rendered + * in the DOM. + */ + private double calculateHeight() { + final int tableHeight = tableWrapper.getOffsetHeight(); + final double footerHeight = footer.heightOfSection; + final double headerHeight = header.heightOfSection; + return tableHeight - footerHeight - headerHeight; + } + + @Override + public void refreshRows(final int index, final int numberOfRows) { + Profiler.enter("Escalator.BodyRowContainer.refreshRows"); + + final Range visualRange = convertToVisual(Range.withLength(index, + numberOfRows)); + + if (!visualRange.isEmpty()) { + final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder + .getFirst()); + for (int rowNumber = visualRange.getStart(); rowNumber < visualRange + .getEnd(); rowNumber++) { + refreshRow(visualRowOrder.get(rowNumber), + firstLogicalRowIndex + rowNumber); + } + } + + Profiler.leave("Escalator.BodyRowContainer.refreshRows"); + } + + @Override + protected TableRowElement getTrByVisualIndex(final int index) + throws IndexOutOfBoundsException { + if (index >= 0 && index < visualRowOrder.size()) { + return visualRowOrder.get(index); + } else { + throw new IndexOutOfBoundsException("No such visual index: " + + index); + } + } + + @Override + public TableRowElement getRowElement(int index) { + if (index < 0 || index >= getRowCount()) { + throw new IndexOutOfBoundsException("No such logical index: " + + index); + } + int visualIndex = index + - getLogicalRowIndex(visualRowOrder.getFirst()); + if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) { + return super.getRowElement(visualIndex); + } else { + throw new IllegalStateException("Row with logical index " + + index + " is currently not available in the DOM"); + } + } + + private void setBodyScrollPosition(final double scrollLeft, + final double scrollTop) { + tBodyScrollLeft = scrollLeft; + tBodyScrollTop = scrollTop; + position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); + } + + /** + * Make sure that there is a correct amount of escalator rows: Add more + * if needed, or remove any superfluous ones. + * <p> + * This method should be called when e.g. the height of the Escalator + * changes. + * <p> + * <em>Note:</em> This method will make sure that the escalator rows are + * placed in the proper places. By default new rows are added below, but + * if the content is scrolled down, the rows are populated on top + * instead. + */ + public void verifyEscalatorCount() { + /* + * This method indeed has a smell very similar to paintRemoveRows + * and paintInsertRows. + * + * Unfortunately, those the code can't trivially be shared, since + * there are some slight differences in the respective + * responsibilities. The "paint" methods fake the addition and + * removal of rows, and make sure to either push existing data out + * of view, or draw new data into view. Only in some special cases + * will the DOM element count change. + * + * This method, however, has the explicit responsibility to verify + * that when "something" happens, we still have the correct amount + * of escalator rows in the DOM, and if not, we make sure to modify + * that count. Only in some special cases do we need to take into + * account other things than simply modifying the DOM element count. + */ + + Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount"); + + if (!isAttached()) { + return; + } + + final int maxEscalatorRows = getMaxEscalatorRowCapacity(); + final int neededEscalatorRows = Math.min(maxEscalatorRows, + body.getRowCount()); + final int neededEscalatorRowsDiff = neededEscalatorRows + - visualRowOrder.size(); + + if (neededEscalatorRowsDiff > 0) { + // needs more + + /* + * This is a workaround for the issue where we might be scrolled + * to the bottom, and the widget expands beyond the content + * range + */ + + final int index = visualRowOrder.size(); + final int nextLastLogicalIndex; + if (!visualRowOrder.isEmpty()) { + nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + 1; + } else { + nextLastLogicalIndex = 0; + } + + final boolean contentWillFit = nextLastLogicalIndex < getRowCount() + - neededEscalatorRowsDiff; + if (contentWillFit) { + final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded( + index, neededEscalatorRowsDiff); + + /* + * Since fillAndPopulateEscalatorRowsIfNeeded operates on + * the assumption that index == visual index == logical + * index, we thank for the added escalator rows, but since + * they're painted in the wrong CSS position, we need to + * move them to their actual locations. + * + * Note: this is the second (see body.paintInsertRows) + * occasion where fillAndPopulateEscalatorRowsIfNeeded would + * behave "more correctly" if it only would add escalator + * rows to the DOM and appropriate bookkeping, and not + * actually populate them :/ + */ + moveAndUpdateEscalatorRows( + Range.withLength(index, addedRows.size()), index, + nextLastLogicalIndex); + } else { + /* + * TODO [[optimize]] + * + * We're scrolled so far down that all rows can't be simply + * appended at the end, since we might start displaying + * escalator rows that don't exist. To avoid the mess that + * is body.paintRemoveRows, this is a dirty hack that dumbs + * the problem down to a more basic and already-solved + * problem: + * + * 1) scroll all the way up 2) add the missing escalator + * rows 3) scroll back to the original position. + * + * Letting the browser scroll back to our original position + * will automatically solve any possible overflow problems, + * since the browser will not allow us to scroll beyond the + * actual content. + */ + + final double oldScrollTop = getScrollTop(); + setScrollTop(0); + scroller.onScroll(); + fillAndPopulateEscalatorRowsIfNeeded(index, + neededEscalatorRowsDiff); + setScrollTop(oldScrollTop); + scroller.onScroll(); + internalScrollEventCalls++; + } + } + + else if (neededEscalatorRowsDiff < 0) { + // needs less + + final ListIterator<TableRowElement> iter = visualRowOrder + .listIterator(visualRowOrder.size()); + for (int i = 0; i < -neededEscalatorRowsDiff; i++) { + final Element last = iter.previous(); + last.removeFromParent(); + iter.remove(); + } + + /* + * If we were scrolled to the bottom so that we didn't have an + * extra escalator row at the bottom, we'll probably end up with + * blank space at the bottom of the escalator, and one extra row + * above the header. + * + * Experimentation idea #1: calculate "scrollbottom" vs content + * bottom and remove one row from top, rest from bottom. This + * FAILED, since setHeight has already happened, thus we never + * will detect ourselves having been scrolled all the way to the + * bottom. + */ + + if (!visualRowOrder.isEmpty()) { + final int firstRowTop = getRowTop(visualRowOrder.getFirst()); + /* + * FIXME [[rowheight]]: coded to work only with default row + * heights - will not work with variable row heights + */ + final double firstRowMinTop = tBodyScrollTop + - getDefaultRowHeight(); + if (firstRowTop < firstRowMinTop) { + final int newLogicalIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + 1; + moveAndUpdateEscalatorRows(Range.withOnly(0), + visualRowOrder.size(), newLogicalIndex); + } + } + } + + if (neededEscalatorRowsDiff != 0) { + fireRowVisibilityChangeEvent(); + } + + Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount"); + } + + @Override + protected void reapplyDefaultRowHeights() { + if (visualRowOrder.isEmpty()) { + return; + } + + /* + * As an intermediate step between hard-coded row heights to crazily + * varying row heights, Escalator will support the modification of + * the default row height (which is applied to all rows). + * + * This allows us to do some assumptions and simplifications for + * now. This code is intended to be quite short-lived, but gives + * insight into what needs to be done when row heights change in the + * body, in a general sense. + * + * TODO [[rowheight]] remove this comment once row heights may + * genuinely vary. + */ + + Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights"); + + /* step 1: resize and reposition rows */ + for (int i = 0; i < visualRowOrder.size(); i++) { + TableRowElement tr = visualRowOrder.get(i); + reapplyRowHeight(tr, getDefaultRowHeight()); + + final int logicalIndex = getTopRowLogicalIndex() + i; + setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight()); + } + + /* + * step 2: move scrollbar so that it corresponds to its previous + * place + */ + + /* + * This ratio needs to be calculated with the scrollsize (not max + * scroll position) in order to align the top row with the new + * scroll position. + */ + double scrollRatio = verticalScrollbar.getScrollPos() + / verticalScrollbar.getScrollSize(); + scroller.recalculateScrollbarsForVirtualViewport(); + internalScrollEventCalls++; + verticalScrollbar.setScrollPos((int) (getDefaultRowHeight() + * getRowCount() * scrollRatio)); + setBodyScrollPosition(horizontalScrollbar.getScrollPos(), + verticalScrollbar.getScrollPos()); + scroller.onScroll(); + + /* step 3: make sure we have the correct amount of escalator rows. */ + verifyEscalatorCount(); + + /* + * TODO [[rowheight]] This simply doesn't work with variable rows + * heights. + */ + setTopRowLogicalIndex(getRowTop(visualRowOrder.getFirst()) + / getDefaultRowHeight()); + + Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights"); + } + + /** + * Sorts the rows in the DOM to correspond to the visual order. + * + * @see #visualRowOrder + */ + private void sortDomElements() { + final String profilingName = "Escalator.BodyRowContainer.sortDomElements"; + Profiler.enter(profilingName); + + /* + * Focus is lost from an element if that DOM element is (or any of + * its parents are) removed from the document. Therefore, we sort + * everything around that row instead. + */ + final TableRowElement activeRow = getEscalatorRowWithFocus(); + + if (activeRow != null) { + assert activeRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body"; + assert visualRowOrder.contains(activeRow) : "Trying to sort around a row that doesn't exist in visualRowOrder."; + } + + /* + * Two cases handled simultaneously: + * + * 1) No focus on rows. We iterate visualRowOrder backwards, and + * take the respective element in the DOM, and place it as the first + * child in the body element. Then we take the next-to-last from + * visualRowOrder, and put that first, pushing the previous row as + * the second child. And so on... + * + * 2) Focus on some row within Escalator body. Again, we iterate + * visualRowOrder backwards. This time, we use the focused row as a + * pivot: Instead of placing rows from the bottom of visualRowOrder + * and placing it first, we place it underneath the focused row. + * Once we hit the focused row, we don't move it (to not reset + * focus) but change sorting mode. After that, we place all rows as + * the first child. + */ + + /* + * If we have a focused row, start in the mode where we put + * everything underneath that row. Otherwise, all rows are placed as + * first child. + */ + boolean insertFirst = (activeRow == null); + + final ListIterator<TableRowElement> i = visualRowOrder + .listIterator(visualRowOrder.size()); + while (i.hasPrevious()) { + TableRowElement tr = i.previous(); + + if (tr == activeRow) { + insertFirst = true; + } else if (insertFirst) { + root.insertFirst(tr); + } else { + root.insertAfter(tr, activeRow); + } + } + + Profiler.leave(profilingName); + } + + /** + * Get the escalator row that has focus. + * + * @return The escalator row that contains a focused DOM element, or + * <code>null</code> if focus is outside of a body row. + */ + private TableRowElement getEscalatorRowWithFocus() { + TableRowElement activeRow = null; + + final Element activeElement = Util.getFocusedElement(); + + if (root.isOrHasChild(activeElement)) { + Element e = activeElement; + + while (e != null && e != root) { + /* + * You never know if there's several tables embedded in a + * cell... We'll take the deepest one. + */ + if (TableRowElement.is(e)) { + activeRow = TableRowElement.as(e); + } + e = e.getParentElement(); + } + } + + return activeRow; + } + + @Override + public Cell getCell(Element element) { + Cell cell = super.getCell(element); + if (cell == null) { + return null; + } + + // Convert DOM coordinates to logical coordinates for rows + Element rowElement = cell.getElement().getParentElement(); + return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), + cell.getElement()); + } + } + + private class ColumnConfigurationImpl implements ColumnConfiguration { + public class Column { + private static final int DEFAULT_COLUMN_WIDTH_PX = 100; + + private int definedWidth = -1; + private int calculatedWidth = DEFAULT_COLUMN_WIDTH_PX; + + public void setWidth(int px) { + definedWidth = px; + calculatedWidth = (px >= 0) ? px : DEFAULT_COLUMN_WIDTH_PX; + } + + public int getDefinedWidth() { + return definedWidth; + } + + public int getCalculatedWidth() { + return calculatedWidth; + } + } + + private final List<Column> columns = new ArrayList<Column>(); + private int frozenColumns = 0; + + /** + * A cached array of all the calculated column widths. + * + * @see #getCalculatedColumnWidths() + */ + private int[] widthsArray = null; + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there are no rows in the DOM when + * this method is called. + * + * @see #hasSomethingInDom() + */ + @Override + public void removeColumns(final int index, final int numberOfColumns) { + assertArgumentsAreValidAndWithinRange(index, numberOfColumns); + + flyweightRow.removeCells(index, numberOfColumns); + + // Cope with removing frozen columns + if (index < frozenColumns) { + if (index + numberOfColumns < frozenColumns) { + /* + * Last removed column was frozen, meaning that all removed + * columns were frozen. Just decrement the number of frozen + * columns accordingly. + */ + frozenColumns -= numberOfColumns; + } else { + /* + * If last removed column was not frozen, we have removed + * columns beyond the frozen range, so all remaining frozen + * columns are to the left of the removed columns. + */ + frozenColumns = index; + } + } + + List<Column> removedColumns = new ArrayList<Column>(); + for (int i = 0; i < numberOfColumns; i++) { + removedColumns.add(columns.remove(index)); + } + + if (hasSomethingInDom()) { + for (final AbstractRowContainer rowContainer : rowContainers) { + rowContainer.paintRemoveColumns(index, numberOfColumns, + removedColumns); + } + } + } + + /** + * Calculate the width of a row, as the sum of columns' widths. + * + * @return the width of a row, in pixels + */ + public int calculateRowWidth() { + return getCalculatedColumnsWidth(Range.between(0, getColumnCount())); + } + + private void assertArgumentsAreValidAndWithinRange(final int index, + final int numberOfColumns) { + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns can't be less than 1 (was " + + numberOfColumns + ")"); + } + + if (index < 0 || index + numberOfColumns > getColumnCount()) { + throw new IndexOutOfBoundsException("The given " + + "column range (" + index + ".." + + (index + numberOfColumns) + + ") was outside of the current " + + "number of columns (" + getColumnCount() + ")"); + } + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for rows when this + * method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void insertColumns(final int index, final int numberOfColumns) { + if (index < 0 || index > getColumnCount()) { + throw new IndexOutOfBoundsException("The given index(" + index + + ") was outside of the current number of columns (0.." + + getColumnCount() + ")"); + } + + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns must be 1 or greater (was " + + numberOfColumns); + } + + flyweightRow.addCells(index, numberOfColumns); + + for (int i = 0; i < numberOfColumns; i++) { + columns.add(index, new Column()); + } + + // Either all or none of the new columns are frozen + boolean frozen = index < frozenColumns; + if (frozen) { + frozenColumns += numberOfColumns; + } + + if (hasColumnAndRowData()) { + for (final AbstractRowContainer rowContainer : rowContainers) { + rowContainer.paintInsertColumns(index, numberOfColumns, + frozen); + } + } + } + + @Override + public int getColumnCount() { + return columns.size(); + } + + @Override + public void setFrozenColumnCount(int count) + throws IllegalArgumentException { + if (count < 0 || count > getColumnCount()) { + throw new IllegalArgumentException( + "count must be between 0 and the current number of columns (" + + columns + ")"); + } + int oldCount = frozenColumns; + if (count == oldCount) { + return; + } + + frozenColumns = count; + + if (hasSomethingInDom()) { + // Are we freezing or unfreezing? + boolean frozen = count > oldCount; + + int firstAffectedCol; + int firstUnaffectedCol; + + if (frozen) { + firstAffectedCol = oldCount; + firstUnaffectedCol = count; + } else { + firstAffectedCol = count; + firstUnaffectedCol = oldCount; + } + + for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) { + header.setColumnFrozen(col, frozen); + body.setColumnFrozen(col, frozen); + footer.setColumnFrozen(col, frozen); + } + } + + scroller.recalculateScrollbarsForVirtualViewport(); + } + + @Override + public int getFrozenColumnCount() { + return frozenColumns; + } + + @Override + public void setColumnWidth(int index, int px) + throws IllegalArgumentException { + checkValidColumnIndex(index); + + columns.get(index).setWidth(px); + widthsArray = null; + + /* + * TODO [[optimize]]: only modify the elements that are actually + * modified. + */ + header.reapplyColumnWidths(); + body.reapplyColumnWidths(); + footer.reapplyColumnWidths(); + recalculateElementSizes(); + } + + private void checkValidColumnIndex(int index) + throws IllegalArgumentException { + if (!Range.withLength(0, getColumnCount()).contains(index)) { + throw new IllegalArgumentException("The given column index (" + + index + ") does not exist"); + } + } + + @Override + public int getColumnWidth(int index) throws IllegalArgumentException { + checkValidColumnIndex(index); + return columns.get(index).getDefinedWidth(); + } + + @Override + public int getColumnWidthActual(int index) { + return columns.get(index).getCalculatedWidth(); + } + + /** + * Calculates the width of the columns in a given range. + * + * @param columns + * the columns to calculate + * @return the total width of the columns in the given + * <code>columns</code> + */ + int getCalculatedColumnsWidth(@SuppressWarnings("hiding") + final Range columns) { + /* + * This is an assert instead of an exception, since this is an + * internal method. + */ + assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range " + + "was outside of current column range (i.e.: " + + Range.between(0, getColumnCount()) + + ", but was given :" + + columns; + + int sum = 0; + for (int i = columns.getStart(); i < columns.getEnd(); i++) { + sum += getColumnWidthActual(i); + } + return sum; + } + + void setCalculatedColumnWidth(int index, int width) { + columns.get(index).calculatedWidth = width; + widthsArray = null; + } + + int[] getCalculatedColumnWidths() { + if (widthsArray == null || widthsArray.length != getColumnCount()) { + widthsArray = new int[getColumnCount()]; + for (int i = 0; i < columns.size(); i++) { + widthsArray[i] = columns.get(i).getCalculatedWidth(); + } + } + return widthsArray; + } + } + + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y + /** + * The solution to + * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 30</code> + * . + * <p> + * This constant is placed in the Escalator class, instead of an inner + * class, since even mathematical expressions aren't allowed in non-static + * inner classes for constants. + */ + private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3); + /** + * The solution to + * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 40</code> + * . + * <p> + * This constant is placed in the Escalator class, instead of an inner + * class, since even mathematical expressions aren't allowed in non-static + * inner classes for constants. + */ + private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9); + + private static final String DEFAULT_WIDTH = "500.0px"; + private static final String DEFAULT_HEIGHT = "400.0px"; + + private FlyweightRow flyweightRow = new FlyweightRow(); + + /** The {@code <thead/>} tag. */ + private final TableSectionElement headElem = TableSectionElement.as(DOM + .createTHead()); + /** The {@code <tbody/>} tag. */ + private final TableSectionElement bodyElem = TableSectionElement.as(DOM + .createTBody()); + /** The {@code <tfoot/>} tag. */ + private final TableSectionElement footElem = TableSectionElement.as(DOM + .createTFoot()); + + /** + * TODO: investigate whether this field is now unnecessary, as + * {@link ScrollbarBundle} now caches its values. + * + * @deprecated maybe... + */ + @Deprecated + private double tBodyScrollTop = 0; + + /** + * TODO: investigate whether this field is now unnecessary, as + * {@link ScrollbarBundle} now caches its values. + * + * @deprecated maybe... + */ + @Deprecated + private double tBodyScrollLeft = 0; + + private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle(); + private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); + + private final HeaderRowContainer header = new HeaderRowContainer(headElem); + private final BodyRowContainer body = new BodyRowContainer(bodyElem); + private final FooterRowContainer footer = new FooterRowContainer(footElem); + + private final Scroller scroller = new Scroller(); + + private final AbstractRowContainer[] rowContainers = new AbstractRowContainer[] { + header, body, footer }; + + private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl(); + private final Element tableWrapper; + + private PositionFunction position; + + private int internalScrollEventCalls = 0; + + /** The cached width of the escalator, in pixels. */ + private double widthOfEscalator; + /** The cached height of the escalator, in pixels. */ + private double heightOfEscalator; + + /** The height of Escalator in terms of body rows. */ + private double heightByRows = GridState.DEFAULT_HEIGHT_BY_ROWS; + + /** The height of Escalator, as defined by {@link #setHeight(String)} */ + private String heightByCss = ""; + + private HeightMode heightMode = HeightMode.CSS; + + private static native double getPreciseWidth(Element element) + /*-{ + if (element.getBoundingClientRect) { + var rect = element.getBoundingClientRect(); + return rect.right - rect.left; + } else { + return element.offsetWidth; + } + }-*/; + + private static native double getPreciseHeight(Element element) + /*-{ + if (element.getBoundingClientRect) { + var rect = element.getBoundingClientRect(); + return rect.bottom - rect.top; + } else { + return element.offsetHeight; + } + }-*/; + + /** + * Creates a new Escalator widget instance. + */ + public Escalator() { + + detectAndApplyPositionFunction(); + getLogger().info( + "Using " + position.getClass().getSimpleName() + + " for position"); + + final Element root = DOM.createDiv(); + setElement(root); + + root.appendChild(verticalScrollbar.getElement()); + verticalScrollbar.getElement().setTabIndex(-1); + verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize()); + + root.appendChild(horizontalScrollbar.getElement()); + horizontalScrollbar.getElement().setTabIndex(-1); + horizontalScrollbar + .setScrollbarThickness(Util.getNativeScrollbarSize()); + horizontalScrollbar + .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() { + @Override + public void visibilityChanged( + ScrollbarBundle.VisibilityChangeEvent event) { + /* + * We either lost or gained a scrollbar. In any case, we + * need to change the height, if it's defined by rows. + */ + applyHeightByRows(); + } + }); + + tableWrapper = DOM.createDiv(); + + root.appendChild(tableWrapper); + + final Element table = DOM.createTable(); + tableWrapper.appendChild(table); + + table.appendChild(headElem); + table.appendChild(bodyElem); + table.appendChild(footElem); + + setStylePrimaryName("v-escalator"); + + // init default dimensions + setHeight(null); + setWidth(null); + } + + @Override + protected void onLoad() { + super.onLoad(); + + header.autodetectRowHeight(); + body.autodetectRowHeight(); + footer.autodetectRowHeight(); + + header.paintInsertRows(0, header.getRowCount()); + footer.paintInsertRows(0, footer.getRowCount()); + recalculateElementSizes(); + /* + * Note: There's no need to explicitly insert rows into the body. + * + * recalculateElementSizes will recalculate the height of the body. This + * has the side-effect that as the body's size grows bigger (i.e. from 0 + * to its actual height), more escalator rows are populated. Those + * escalator rows are then immediately rendered. This, in effect, is the + * same thing as inserting those rows. + * + * In fact, having an extra paintInsertRows here would lead to duplicate + * rows. + */ + + scroller.attachScrollListener(verticalScrollbar.getElement()); + scroller.attachScrollListener(horizontalScrollbar.getElement()); + scroller.attachMousewheelListener(getElement()); + scroller.attachTouchListeners(getElement()); + } + + @Override + protected void onUnload() { + + scroller.detachScrollListener(verticalScrollbar.getElement()); + scroller.detachScrollListener(horizontalScrollbar.getElement()); + scroller.detachMousewheelListener(getElement()); + scroller.detachTouchListeners(getElement()); + + header.paintRemoveRows(0, header.getRowCount()); + footer.paintRemoveRows(0, footer.getRowCount()); + body.paintRemoveRows(0, body.getRowCount()); + + super.onUnload(); + } + + private void detectAndApplyPositionFunction() { + /* + * firefox has a bug in its translate operation, showing white space + * when adjusting the scrollbar in BodyRowContainer.paintInsertRows + */ + if (Window.Navigator.getUserAgent().contains("Firefox")) { + position = new AbsolutePosition(); + return; + } + + final Style docStyle = Document.get().getBody().getStyle(); + if (hasProperty(docStyle, "transform")) { + if (hasProperty(docStyle, "transformStyle")) { + position = new Translate3DPosition(); + } else { + position = new TranslatePosition(); + } + } else if (hasProperty(docStyle, "webkitTransform")) { + position = new WebkitTranslate3DPosition(); + } else { + position = new AbsolutePosition(); + } + } + + private Logger getLogger() { + return Logger.getLogger(getClass().getName()); + } + + private static native boolean hasProperty(Style style, String name) + /*-{ + return style[name] !== undefined; + }-*/; + + /** + * Check whether there are both columns and any row data (for either + * headers, body or footer). + * + * @return <code>true</code> iff header, body or footer has rows && there + * are columns + */ + private boolean hasColumnAndRowData() { + return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer + .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0; + } + + /** + * Check whether there are any cells in the DOM. + * + * @return <code>true</code> iff header, body or footer has any child + * elements + */ + private boolean hasSomethingInDom() { + return headElem.hasChildNodes() || bodyElem.hasChildNodes() + || footElem.hasChildNodes(); + } + + /** + * Returns the row container for the header in this Escalator. + * + * @return the header. Never <code>null</code> + */ + public RowContainer getHeader() { + return header; + } + + /** + * Returns the row container for the body in this Escalator. + * + * @return the body. Never <code>null</code> + */ + public RowContainer getBody() { + return body; + } + + /** + * Returns the row container for the footer in this Escalator. + * + * @return the footer. Never <code>null</code> + */ + public RowContainer getFooter() { + return footer; + } + + /** + * Returns the configuration object for the columns in this Escalator. + * + * @return the configuration object for the columns in this Escalator. Never + * <code>null</code> + */ + public ColumnConfiguration getColumnConfiguration() { + return columnConfiguration; + } + + @Override + public void setWidth(final String width) { + super.setWidth(width != null && !width.isEmpty() ? width + : DEFAULT_WIDTH); + recalculateElementSizes(); + } + + /** + * {@inheritDoc} + * <p> + * If Escalator is currently not in {@link HeightMode#CSS}, the given value + * is remembered, and applied once the mode is applied. + * + * @see #setHeightMode(HeightMode) + */ + @Override + public void setHeight(String height) { + /* + * TODO remove method once RequiresResize and the Vaadin layoutmanager + * listening mechanisms are implemented + */ + + heightByCss = height; + if (getHeightMode() == HeightMode.CSS) { + setHeightInternal(height); + } + } + + private void setHeightInternal(final String height) { + final int escalatorRowsBefore = body.visualRowOrder.size(); + + super.setHeight(height != null && !height.isEmpty() ? height + : DEFAULT_HEIGHT); + recalculateElementSizes(); + + if (escalatorRowsBefore != body.visualRowOrder.size()) { + fireRowVisibilityChangeEvent(); + } + } + + /** + * Returns the vertical scroll offset. Note that this is not necessarily the + * same as the {@code scrollTop} attribute in the DOM. + * + * @return the logical vertical scroll offset + */ + public double getScrollTop() { + return verticalScrollbar.getScrollPos(); + } + + /** + * Sets the vertical scroll offset. Note that this will not necessarily + * become the same as the {@code scrollTop} attribute in the DOM. + * + * @param scrollTop + * the number of pixels to scroll vertically + */ + public void setScrollTop(final double scrollTop) { + verticalScrollbar.setScrollPos(scrollTop); + } + + /** + * Returns the logical horizontal scroll offset. Note that this is not + * necessarily the same as the {@code scrollLeft} attribute in the DOM. + * + * @return the logical horizontal scroll offset + */ + public double getScrollLeft() { + return horizontalScrollbar.getScrollPos(); + } + + /** + * Sets the logical horizontal scroll offset. Note that will not necessarily + * become the same as the {@code scrollLeft} attribute in the DOM. + * + * @param scrollLeft + * the number of pixels to scroll horizontally + */ + public void setScrollLeft(final double scrollLeft) { + horizontalScrollbar.setScrollPos(scrollLeft); + } + + /** + * Scrolls the body horizontally so that the column at the given index is + * visible and there is at least {@code padding} pixels in the direction of + * the given scroll destination. + * + * @param columnIndex + * the index of the column to scroll to + * @param destination + * where the column should be aligned visually after scrolling + * @param padding + * the number pixels to place between the scrolled-to column and + * the viewport edge. + * @throws IndexOutOfBoundsException + * if {@code columnIndex} is not a valid index for an existing + * column + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, or if the indicated column is frozen + */ + public void scrollToColumn(final int columnIndex, + final ScrollDestination destination, final int padding) + throws IndexOutOfBoundsException, IllegalArgumentException { + if (destination == ScrollDestination.MIDDLE && padding != 0) { + throw new IllegalArgumentException( + "You cannot have a padding with a MIDDLE destination"); + } + verifyValidColumnIndex(columnIndex); + + if (columnIndex < columnConfiguration.frozenColumns) { + throw new IllegalArgumentException("The given column index " + + columnIndex + " is frozen."); + } + + scroller.scrollToColumn(columnIndex, destination, padding); + } + + private void verifyValidColumnIndex(final int columnIndex) + throws IndexOutOfBoundsException { + if (columnIndex < 0 + || columnIndex >= columnConfiguration.getColumnCount()) { + throw new IndexOutOfBoundsException("The given column index " + + columnIndex + " does not exist."); + } + } + + /** + * Scrolls the body vertically so that the row at the given index is visible + * and there is at least {@literal padding} pixels to the given scroll + * destination. + * + * @param rowIndex + * the index of the logical row to scroll to + * @param destination + * where the row should be aligned visually after scrolling + * @param padding + * the number pixels to place between the scrolled-to row and the + * viewport edge. + * @throws IndexOutOfBoundsException + * if {@code rowIndex} is not a valid index for an existing row + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero + */ + public void scrollToRow(final int rowIndex, + final ScrollDestination destination, final int padding) + throws IndexOutOfBoundsException, IllegalArgumentException { + if (destination == ScrollDestination.MIDDLE && padding != 0) { + throw new IllegalArgumentException( + "You cannot have a padding with a MIDDLE destination"); + } + verifyValidRowIndex(rowIndex); + + scroller.scrollToRow(rowIndex, destination, padding); + } + + private void verifyValidRowIndex(final int rowIndex) { + if (rowIndex < 0 || rowIndex >= body.getRowCount()) { + throw new IndexOutOfBoundsException("The given row index " + + rowIndex + " does not exist."); + } + } + + /** + * Recalculates the dimensions for all elements that require manual + * calculations. Also updates the dimension caches. + * <p> + * <em>Note:</em> This method has the <strong>side-effect</strong> + * automatically makes sure that an appropriate amount of escalator rows are + * present. So, if the body area grows, more <strong>escalator rows might be + * inserted</strong>. Conversely, if the body area shrinks, + * <strong>escalator rows might be removed</strong>. + */ + private void recalculateElementSizes() { + if (!isAttached()) { + return; + } + + Profiler.enter("Escalator.recalculateElementSizes"); + widthOfEscalator = getPreciseWidth(getElement()); + heightOfEscalator = getPreciseHeight(getElement()); + for (final AbstractRowContainer rowContainer : rowContainers) { + rowContainer.recalculateSectionHeight(); + } + + scroller.recalculateScrollbarsForVirtualViewport(); + body.verifyEscalatorCount(); + Profiler.leave("Escalator.recalculateElementSizes"); + } + + /** + * A routing method for {@link Scroller#onScroll()}. + * <p> + * This is a workaround for GWT and JSNI unable to properly handle inner + * classes, so instead we call the outer class' method, which calls the + * inner class' respective method. + * <p> + * Ideally, this method would not exist, and {@link Scroller#onScroll()} + * would be called directly. + */ + private void onScroll() { + scroller.onScroll(); + } + + /** + * Snap deltas of x and y to the major four axes (up, down, left, right) + * with a threshold of a number of degrees from those axes. + * + * @param deltaX + * the delta in the x axis + * @param deltaY + * the delta in the y axis + * @param thresholdRatio + * the threshold in ratio (0..1) between x and y for when to snap + * @return a two-element array: <code>[snappedX, snappedY]</code> + */ + private static double[] snapDeltas(final double deltaX, + final double deltaY, final double thresholdRatio) { + + final double[] array = new double[2]; + if (deltaX != 0 && deltaY != 0) { + final double aDeltaX = Math.abs(deltaX); + final double aDeltaY = Math.abs(deltaY); + final double yRatio = aDeltaY / aDeltaX; + final double xRatio = aDeltaX / aDeltaY; + + array[0] = (xRatio < thresholdRatio) ? 0 : deltaX; + array[1] = (yRatio < thresholdRatio) ? 0 : deltaY; + } else { + array[0] = deltaX; + array[1] = deltaY; + } + + return array; + } + + /** + * Adds an event handler that gets notified when the range of visible rows + * changes e.g. because of scrolling or row resizing. + * + * @param rowVisibilityChangeHandler + * the event handler + * @return a handler registration for the added handler + */ + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler rowVisibilityChangeHandler) { + return addHandler(rowVisibilityChangeHandler, + RowVisibilityChangeEvent.TYPE); + } + + private void fireRowVisibilityChangeEvent() { + if (!body.visualRowOrder.isEmpty()) { + int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder + .getFirst()); + int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder + .getLast()) + 1; + + int visibleRowCount = visibleRangeEnd - visibleRangeStart; + fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, + visibleRowCount)); + } else { + fireEvent(new RowVisibilityChangeEvent(0, 0)); + } + } + + /** + * Gets the range of currently visible rows. + * + * @return range of visible rows + */ + public Range getVisibleRowRange() { + return Range.withLength( + body.getLogicalRowIndex(body.visualRowOrder.getFirst()), + body.visualRowOrder.size()); + } + + /** + * Returns the widget from a cell node or <code>null</code> if there is no + * widget in the cell + * + * @param cellNode + * The cell node + */ + static Widget getWidgetFromCell(Node cellNode) { + Node possibleWidgetNode = cellNode.getFirstChild(); + if (possibleWidgetNode != null + && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) { + @SuppressWarnings("deprecation") + com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode + .cast(); + Widget w = Util.findWidget(castElement, null); + + // Ensure findWidget did not traverse past the cell element in the + // DOM hierarchy + if (cellNode.isOrHasChild(w.getElement())) { + return w; + } + } + return null; + } + + /** + * Forces the escalator to recalculate the widths of its columns. + * <p> + * All columns that haven't been assigned an explicit width will be resized + * to fit all currently visible contents. + * + * @see ColumnConfiguration#setColumnWidth(int, int) + */ + public void calculateColumnWidths() { + boolean widthsHaveChanged = false; + for (int colIndex = 0; colIndex < columnConfiguration.getColumnCount(); colIndex++) { + if (columnConfiguration.getColumnWidth(colIndex) >= 0) { + continue; + } + + final int oldColumnWidth = columnConfiguration + .getColumnWidthActual(colIndex); + + int maxColumnWidth = 0; + maxColumnWidth = Math.max(maxColumnWidth, + header.calculateMaxColWidth(colIndex)); + maxColumnWidth = Math.max(maxColumnWidth, + body.calculateMaxColWidth(colIndex)); + maxColumnWidth = Math.max(maxColumnWidth, + footer.calculateMaxColWidth(colIndex)); + + Logger.getLogger("Escalator.calculateColumnWidths").info( + "#" + colIndex + ": " + maxColumnWidth + "px"); + + if (oldColumnWidth != maxColumnWidth) { + columnConfiguration.setCalculatedColumnWidth(colIndex, + maxColumnWidth); + widthsHaveChanged = true; + } + } + + if (widthsHaveChanged) { + header.reapplyColumnWidths(); + body.reapplyColumnWidths(); + footer.reapplyColumnWidths(); + recalculateElementSizes(); + } + } + + @Override + public void setStylePrimaryName(String style) { + super.setStylePrimaryName(style); + + verticalScrollbar.setStylePrimaryName(style); + horizontalScrollbar.setStylePrimaryName(style); + + UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper"); + + header.setStylePrimaryName(style); + body.setStylePrimaryName(style); + footer.setStylePrimaryName(style); + } + + /** + * Sets the number of rows that should be visible in Escalator's body, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * If Escalator is currently not in {@link HeightMode#ROW}, the given value + * is remembered, and applied once the mode is applied. + * + * @param rows + * the number of rows that should be visible in Escalator's body + * @throws IllegalArgumentException + * if {@code rows} is ≤ 0, + * {@link Double#isInifinite(double) infinite} or + * {@link Double#isNaN(double) NaN}. + * @see #setHeightMode(HeightMode) + */ + public void setHeightByRows(double rows) throws IllegalArgumentException { + if (rows <= 0) { + throw new IllegalArgumentException( + "The number of rows must be a positive number."); + } else if (Double.isInfinite(rows)) { + throw new IllegalArgumentException( + "The number of rows must be finite."); + } else if (Double.isNaN(rows)) { + throw new IllegalArgumentException("The number must not be NaN."); + } + + heightByRows = rows; + applyHeightByRows(); + } + + /** + * Gets the amount of rows in Escalator's body that are shown, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * By default, it is {@value GridState#DEFAULT_HEIGHT_BY_ROWS}. + * + * @return the amount of rows that are being shown in Escalator's body + * @see #setHeightByRows(double) + */ + public double getHeightByRows() { + return heightByRows; + } + + /** + * Reapplies the row-based height of the Grid, if Grid currently should + * define its height that way. + */ + private void applyHeightByRows() { + if (heightMode != HeightMode.ROW) { + return; + } + + double headerHeight = header.heightOfSection; + double footerHeight = footer.heightOfSection; + double bodyHeight = body.getDefaultRowHeight() * heightByRows; + double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar + .getScrollbarThickness() : 0; + + double totalHeight = headerHeight + bodyHeight + scrollbar + + footerHeight; + setHeightInternal(totalHeight + "px"); + } + + /** + * Defines the mode in which the Escalator widget's height is calculated. + * <p> + * If {@link HeightMode#CSS} is given, Escalator will respect the values + * given via {@link #setHeight(String)}, and behave as a traditional Widget. + * <p> + * If {@link HeightMode#ROW} is given, Escalator will make sure that the + * {@link #getBody() body} will display as many rows as + * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are + * inserted or removed, the widget will resize itself to still display the + * required amount of rows in its body. It also takes the horizontal + * scrollbar into account. + * + * @param heightMode + * the mode in to which Escalator should be set + */ + public void setHeightMode(HeightMode heightMode) { + /* + * This method is a workaround for the fact that Vaadin re-applies + * widget dimensions (height/width) on each state change event. The + * original design was to have setHeight an setHeightByRow be equals, + * and whichever was called the latest was considered in effect. + * + * But, because of Vaadin always calling setHeight on the widget, this + * approach doesn't work. + */ + + if (heightMode != this.heightMode) { + this.heightMode = heightMode; + + switch (this.heightMode) { + case CSS: + setHeight(heightByCss); + break; + case ROW: + setHeightByRows(heightByRows); + break; + default: + throw new IllegalStateException("Unimplemented feature " + + "- unknown HeightMode: " + this.heightMode); + } + } + } + + /** + * Returns the current {@link HeightMode} the Escalator is in. + * <p> + * Defaults to {@link HeightMode#CSS}. + * + * @return the current HeightMode + */ + public HeightMode getHeightMode() { + return heightMode; + } + + /** + * Returns the {@link RowContainer} which contains the element. + * + * @param element + * the element to check for + * @return the container the element is in or <code>null</code> if element + * is not present in any container. + */ + public RowContainer findRowContainer(Element element) { + if (getHeader().getElement().isOrHasChild(element)) { + return getHeader(); + } else if (getBody().getElement().isOrHasChild(element)) { + return getBody(); + } else if (getFooter().getElement().isOrHasChild(element)) { + return getFooter(); + } + return null; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java new file mode 100644 index 0000000000..aae6b63d20 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java @@ -0,0 +1,155 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +/** + * An interface that allows client code to define how a certain row in Escalator + * will be displayed. The contents of an escalator's header, body and footer are + * rendered by their respective updaters. + * <p> + * The updater is responsible for internally handling all remote communication, + * should the displayed data need to be fetched remotely. + * <p> + * This has a similar function to {@link Grid Grid's} {@link Renderer Renderers} + * , although they operate on different abstraction levels. + * + * @since + * @author Vaadin Ltd + * @see RowContainer#setEscalatorUpdater(EscalatorUpdater) + * @see Escalator#getHeader() + * @see Escalator#getBody() + * @see Escalator#getFooter() + * @see Renderer + */ +public interface EscalatorUpdater { + + /** + * An {@link EscalatorUpdater} that doesn't render anything. + */ + public static final EscalatorUpdater NULL = new EscalatorUpdater() { + @Override + public void update(final Row row, + final Iterable<FlyweightCell> cellsToUpdate) { + // NOOP + } + + @Override + public void preAttach(final Row row, + final Iterable<FlyweightCell> cellsToAttach) { + // NOOP + + } + + @Override + public void postAttach(final Row row, + final Iterable<FlyweightCell> attachedCells) { + // NOOP + } + + @Override + public void preDetach(final Row row, + final Iterable<FlyweightCell> cellsToDetach) { + // NOOP + } + + @Override + public void postDetach(final Row row, + final Iterable<FlyweightCell> detachedCells) { + // NOOP + } + }; + + /** + * Renders a row contained in a row container. + * <p> + * <em>Note:</em> If rendering of cells is deferred (e.g. because + * asynchronous data retrieval), this method is responsible for explicitly + * displaying some placeholder data (empty content is valid). Because the + * cells (and rows) in an escalator are recycled, failing to reset a cell's + * presentation will lead to wrong data being displayed in the escalator. + * <p> + * For performance reasons, the escalator will never autonomously clear any + * data in a cell. + * + * @param row + * Information about the row that is being updated. + * <em>Note:</em> You should not store nor reuse this reference. + * @param cellsToUpdate + * A collection of cells that need to be updated. <em>Note:</em> + * You should neither store nor reuse the reference to the + * iterable, nor to the individual cells. + */ + public void update(Row row, Iterable<FlyweightCell> cellsToUpdate); + + /** + * Called before attaching new cells to the escalator. + * + * @param row + * Information about the row to which the cells will be added. + * <em>Note:</em> You should not store nor reuse this reference. + * @param cellsToAttach + * A collection of cells that are about to be attached. + * <em>Note:</em> You should neither store nor reuse the + * reference to the iterable, nor to the individual cells. + * + */ + public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach); + + /** + * Called after attaching new cells to the escalator. + * + * @param row + * Information about the row to which the cells were added. + * <em>Note:</em> You should not store nor reuse this reference. + * @param attachedCells + * A collection of cells that were attached. <em>Note:</em> You + * should neither store nor reuse the reference to the iterable, + * nor to the individual cells. + * + */ + public void postAttach(Row row, Iterable<FlyweightCell> attachedCells); + + /** + * Called before detaching cells from the escalator. + * + * @param row + * Information about the row from which the cells will be + * removed. <em>Note:</em> You should not store nor reuse this + * reference. + * @param cellsToAttach + * A collection of cells that are about to be detached. + * <em>Note:</em> You should neither store nor reuse the + * reference to the iterable, nor to the individual cells. + * + */ + public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach); + + /** + * Called after detaching cells from the escalator. + * + * @param row + * Information about the row from which the cells were removed. + * <em>Note:</em> You should not store nor reuse this reference. + * @param attachedCells + * A collection of cells that were detached. <em>Note:</em> You + * should neither store nor reuse the reference to the iterable, + * nor to the individual cells. + * + */ + public void postDetach(Row row, Iterable<FlyweightCell> detachedCells); + +} diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightCell.java b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java new file mode 100644 index 0000000000..30cc4fc79e --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java @@ -0,0 +1,194 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import java.util.List; + +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.TableCellElement; +import com.vaadin.client.ui.grid.FlyweightRow.CellIterator; + +/** + * A {@link FlyweightCell} represents a cell in the {@link Grid} or + * {@link Escalator} at a certain point in time. + * + * <p> + * Since the {@link FlyweightCell} follows the <code>Flyweight</code>-pattern + * any instance of this object is subject to change without the user knowing it + * and so should not be stored anywhere outside of the method providing these + * instances. + * + * @since + * @author Vaadin Ltd + */ +public class FlyweightCell { + static final String COLSPAN_ATTR = "colSpan"; + + private final int column; + private final FlyweightRow row; + + private TableCellElement element = null; + private CellIterator currentIterator = null; + + public FlyweightCell(final FlyweightRow row, final int column) { + this.row = row; + this.column = column; + } + + /** + * Returns the row index of the cell + */ + public int getRow() { + assertSetup(); + return row.getRow(); + } + + /** + * Returns the column index of the cell + */ + public int getColumn() { + assertSetup(); + return column; + } + + /** + * Returns the element of the cell. Can be either a <code>TD</code> element + * or a <code>TH</code> element. + */ + public TableCellElement getElement() { + assertSetup(); + return element; + } + + /** + * Return the colspan attribute of the element of the cell. + */ + public int getColSpan() { + assertSetup(); + return element.getPropertyInt(COLSPAN_ATTR); + } + + /** + * Sets the DOM element for this FlyweightCell, either a <code>TD</code> or + * a <code>TH</code>. It is the caller's responsibility to actually insert + * the given element to the document when needed. + * + * @param element + * the element corresponding to this cell, cannot be null + */ + void setElement(TableCellElement element) { + assert element != null; + assertSetup(); + this.element = element; + } + + void setup(final CellIterator iterator) { + currentIterator = iterator; + + if (iterator.areCellsAttached()) { + final TableCellElement e = row.getElement().getCells() + .getItem(column); + e.setPropertyInt(COLSPAN_ATTR, 1); + e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX); + e.getStyle().clearDisplay(); + setElement(e); + } + } + + /** + * Tear down the state of the Cell. + * <p> + * This is an internal check method, to prevent retrieving uninitialized + * data by calling {@link #getRow()}, {@link #getColumn()} or + * {@link #getElement()} at an improper time. + * <p> + * This should only be used with asserts (" + * <code>assert flyweightCell.teardown()</code> ") so that the code is never + * run when asserts aren't enabled. + * + * @return always <code>true</code> + * @see FlyweightRow#teardown() + */ + boolean teardown() { + currentIterator = null; + element = null; + return true; + } + + /** + * Asserts that the flyweight cell has properly been set up before trying to + * access any of its data. + */ + private void assertSetup() { + assert currentIterator != null : "FlyweightCell was not properly " + + "initialized. This is either a bug in Grid/Escalator " + + "or a Cell reference has been stored and reused " + + "inappropriately."; + } + + public void setColSpan(final int numberOfCells) { + if (numberOfCells < 1) { + throw new IllegalArgumentException( + "Number of cells should be more than 0"); + } + + /*- + * This will default to 1 if unset, as per DOM specifications: + * http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements + */ + final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR); + if (numberOfCells == 1 && prevColSpan == 1) { + return; + } + + getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells); + adjustCellWidthForSpan(numberOfCells); + hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan); + currentIterator.setSkipNext(numberOfCells - 1); + } + + private void adjustCellWidthForSpan(final int numberOfCells) { + final int cellsToTheRight = currentIterator.rawPeekNext( + numberOfCells - 1).size(); + + final int selfWidth = row.getColumnWidth(column); + int widthsOfColumnsToTheRight = 0; + for (int i = 0; i < cellsToTheRight; i++) { + widthsOfColumnsToTheRight += row.getColumnWidth(column + i + 1); + } + getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight, + Unit.PX); + } + + private void hideOrRevealAdjacentCellElements(final int numberOfCells, + final int prevColSpan) { + final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells); + final List<FlyweightCell> affectedCells = currentIterator + .rawPeekNext(affectedCellsNumber - 1); + if (prevColSpan < numberOfCells) { + for (int i = 0; i < affectedCells.size(); i++) { + affectedCells.get(prevColSpan + i - 1).getElement().getStyle() + .setDisplay(Display.NONE); + } + } else if (prevColSpan > numberOfCells) { + for (int i = 0; i < affectedCells.size(); i++) { + affectedCells.get(numberOfCells + i - 1).getElement() + .getStyle().clearDisplay(); + } + } + } +} diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightRow.java b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java new file mode 100644 index 0000000000..08f4f1d33c --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java @@ -0,0 +1,293 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.dom.client.TableRowElement; + +/** + * An internal implementation of the {@link Row} interface. + * <p> + * There is only one instance per Escalator. This is designed to be re-used when + * rendering rows. + * + * @since + * @author Vaadin Ltd + * @see Escalator.AbstractRowContainer#refreshRow(Node, int) + */ +class FlyweightRow implements Row { + + static class CellIterator implements Iterator<FlyweightCell> { + /** A defensive copy of the cells in the current row. */ + private final ArrayList<FlyweightCell> cells; + private final boolean cellsAttached; + private int cursor = 0; + private int skipNext = 0; + + /** + * Creates a new iterator of attached flyweight cells. A cell is + * attached if it has a corresponding {@link FlyweightCell#getElement() + * DOM element} attached to the row element. + * + * @param cells + * the collection of cells to iterate + */ + public static CellIterator attached( + final Collection<FlyweightCell> cells) { + return new CellIterator(cells, true); + } + + /** + * Creates a new iterator of unattached flyweight cells. A cell is + * unattached if it does not have a corresponding + * {@link FlyweightCell#getElement() DOM element} attached to the row + * element. + * + * @param cells + * the collection of cells to iterate + */ + public static CellIterator unattached( + final Collection<FlyweightCell> cells) { + return new CellIterator(cells, false); + } + + private CellIterator(final Collection<FlyweightCell> cells, + final boolean attached) { + this.cells = new ArrayList<FlyweightCell>(cells); + cellsAttached = attached; + } + + @Override + public boolean hasNext() { + return cursor + skipNext < cells.size(); + } + + @Override + public FlyweightCell next() { + // if we needed to skip some cells since the last invocation. + for (int i = 0; i < skipNext; i++) { + cells.remove(cursor); + } + skipNext = 0; + + final FlyweightCell cell = cells.get(cursor++); + cell.setup(this); + return cell; + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "Cannot remove cells via iterator"); + } + + /** + * Sets the number of cells to skip when {@link #next()} is called the + * next time. Cell hiding is also handled eagerly in this method. + * + * @param colspan + * the number of cells to skip on next invocation of + * {@link #next()} + */ + public void setSkipNext(final int colspan) { + assert colspan > 0 : "Number of cells didn't make sense: " + + colspan; + skipNext = colspan; + } + + /** + * Gets the next <code>n</code> cells in the iterator, ignoring any + * possibly spanned cells. + * + * @param n + * the number of next cells to retrieve + * @return A list of next <code>n</code> cells, or less if there aren't + * enough cells to retrieve + */ + public List<FlyweightCell> rawPeekNext(final int n) { + final int from = Math.min(cursor, cells.size()); + final int to = Math.min(cursor + n, cells.size()); + List<FlyweightCell> nextCells = cells.subList(from, to); + for (FlyweightCell cell : nextCells) { + cell.setup(this); + } + return nextCells; + } + + public boolean areCellsAttached() { + return cellsAttached; + } + } + + private static final int BLANK = Integer.MIN_VALUE; + + private int row; + private TableRowElement element; + private int[] columnWidths = null; + private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>(); + + void setup(final TableRowElement e, final int row, int[] columnWidths) { + element = e; + this.row = row; + this.columnWidths = columnWidths; + } + + /** + * Tear down the state of the Row. + * <p> + * This is an internal check method, to prevent retrieving uninitialized + * data by calling {@link #getRow()}, {@link #getElement()} or + * {@link #getCells()} at an improper time. + * <p> + * This should only be used with asserts (" + * <code>assert flyweightRow.teardown()</code> ") so that the code is never + * run when asserts aren't enabled. + * + * @return always <code>true</code> + */ + boolean teardown() { + element = null; + row = BLANK; + columnWidths = null; + for (final FlyweightCell cell : cells) { + assert cell.teardown(); + } + return true; + } + + @Override + public int getRow() { + assertSetup(); + return row; + } + + @Override + public TableRowElement getElement() { + assertSetup(); + return element; + } + + void addCells(final int index, final int numberOfColumns) { + for (int i = 0; i < numberOfColumns; i++) { + final int col = index + i; + cells.add(col, new FlyweightCell(this, col)); + } + updateRestOfCells(index + numberOfColumns); + } + + void removeCells(final int index, final int numberOfColumns) { + for (int i = 0; i < numberOfColumns; i++) { + cells.remove(index); + } + updateRestOfCells(index); + } + + private void updateRestOfCells(final int startPos) { + // update the column number for the cells to the right + for (int col = startPos; col < cells.size(); col++) { + cells.set(col, new FlyweightCell(this, col)); + } + } + + /** + * Returns flyweight cells for the client code to render. The cells get + * their associated {@link FlyweightCell#getElement() elements} from the row + * element. + * <p> + * Precondition: each cell has a corresponding element in the row + * + * @return an iterable of flyweight cells + * + * @see #setup(Element, int, int[]) + * @see #teardown() + */ + Iterable<FlyweightCell> getCells() { + return getCells(0, cells.size()); + } + + /** + * Returns a subrange of flyweight cells for the client code to render. The + * cells get their associated {@link FlyweightCell#getElement() elements} + * from the row element. + * <p> + * Precondition: each cell has a corresponding element in the row + * + * @param offset + * the index of the first cell to return + * @param numberOfCells + * the number of cells to return + * @return an iterable of flyweight cells + */ + Iterable<FlyweightCell> getCells(final int offset, final int numberOfCells) { + assertSetup(); + return new Iterable<FlyweightCell>() { + @Override + public Iterator<FlyweightCell> iterator() { + return CellIterator.attached(cells.subList(offset, offset + + numberOfCells)); + } + }; + } + + /** + * Returns a subrange of unattached flyweight cells. Unattached cells do not + * have {@link FlyweightCell#getElement() elements} associated. Note that + * FlyweightRow does not keep track of whether cells in actuality have + * corresponding DOM elements or not; it is the caller's responsibility to + * invoke this method with correct parameters. + * <p> + * Precondition: the range [offset, offset + numberOfCells) must be valid + * + * @param offset + * the index of the first cell to return + * @param numberOfCells + * the number of cells to return + * @return an iterable of flyweight cells + */ + Iterable<FlyweightCell> getUnattachedCells(final int offset, + final int numberOfCells) { + assertSetup(); + assert offset >= 0 && offset + numberOfCells <= cells.size() : "Invalid range of cells"; + return new Iterable<FlyweightCell>() { + @Override + public Iterator<FlyweightCell> iterator() { + return CellIterator.unattached(cells.subList(offset, offset + + numberOfCells)); + } + }; + } + + /** + * Asserts that the flyweight row has properly been set up before trying to + * access any of its data. + */ + private void assertSetup() { + assert element != null && row != BLANK && columnWidths != null : "Flyweight row was not " + + "properly initialized. Make sure the setup-method is " + + "called before retrieving data. This is either a bug " + + "in Escalator, or the instance of the flyweight row " + + "has been stored and accessed."; + } + + int getColumnWidth(int column) { + assertSetup(); + return columnWidths[column]; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java new file mode 100644 index 0000000000..70d8286c90 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -0,0 +1,2562 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.EventTarget; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.Touch; +import com.google.gwt.event.dom.client.KeyCodeEvent; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.touch.client.Point; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HasVisibility; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.Util; +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.SubPartAware; +import com.vaadin.client.ui.grid.GridFooter.FooterRow; +import com.vaadin.client.ui.grid.GridHeader.HeaderRow; +import com.vaadin.client.ui.grid.GridStaticSection.StaticCell; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler; +import com.vaadin.client.ui.grid.keyevents.BodyKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.BodyKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.BodyKeyUpHandler; +import com.vaadin.client.ui.grid.keyevents.FooterKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.FooterKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.FooterKeyUpHandler; +import com.vaadin.client.ui.grid.keyevents.GridKeyDownEvent; +import com.vaadin.client.ui.grid.keyevents.GridKeyPressEvent; +import com.vaadin.client.ui.grid.keyevents.GridKeyUpEvent; +import com.vaadin.client.ui.grid.keyevents.HeaderKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.HeaderKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.HeaderKeyUpHandler; +import com.vaadin.client.ui.grid.renderers.ComplexRenderer; +import com.vaadin.client.ui.grid.renderers.WidgetRenderer; +import com.vaadin.client.ui.grid.selection.HasSelectionChangeHandlers; +import com.vaadin.client.ui.grid.selection.SelectionChangeEvent; +import com.vaadin.client.ui.grid.selection.SelectionChangeHandler; +import com.vaadin.client.ui.grid.selection.SelectionModel; +import com.vaadin.client.ui.grid.selection.SelectionModelMulti; +import com.vaadin.client.ui.grid.selection.SelectionModelNone; +import com.vaadin.client.ui.grid.selection.SelectionModelSingle; +import com.vaadin.client.ui.grid.sort.Sort; +import com.vaadin.client.ui.grid.sort.SortEvent; +import com.vaadin.client.ui.grid.sort.SortEventHandler; +import com.vaadin.client.ui.grid.sort.SortOrder; +import com.vaadin.shared.ui.grid.GridConstants; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.SortDirection; +import com.vaadin.shared.ui.grid.SortEventOriginator; + +/** + * A data grid view that supports columns and lazy loading of data rows from a + * data source. + * + * <h3>Columns</h3> + * <p> + * The {@link GridColumn} class defines the renderer used to render a cell in + * the grid. Implement {@link GridColumn#getValue(Object)} to retrieve the cell + * value from the row object and return the cell renderer to render that cell. + * </p> + * <p> + * {@link GridColumn}s contain other properties like the width of the column and + * the visiblity of the column. If you want to change a column's properties + * after it has been added to the grid you can get a column object for a + * specific column index using {@link Grid#getColumn(int)}. + * </p> + * <p> + * + * TODO Explain about headers/footers once the multiple header/footer api has + * been implemented + * + * <h3>Data sources</h3> + * <p> + * TODO Explain about what a data source is and how it should be implemented. + * </p> + * + * @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 + * @author Vaadin Ltd + */ +public class Grid<T> extends Composite implements + HasSelectionChangeHandlers<T>, SubPartAware { + + public static abstract class AbstractGridKeyEvent<T, HANDLER extends AbstractGridKeyEventHandler> + extends KeyCodeEvent<HANDLER> { + + /** + * Enum describing different section of Grid. + */ + public enum GridSection { + HEADER, BODY, FOOTER + } + + private Grid<T> grid; + protected Cell activeCell; + protected GridSection activeSection; + private final Type<HANDLER> associatedType = new Type<HANDLER>( + getBrowserEventType(), this); + + public AbstractGridKeyEvent(Grid<T> grid) { + this.grid = grid; + } + + protected abstract String getBrowserEventType(); + + /** + * Gets the Grid instance for this event. + * + * @return grid + */ + public Grid<T> getGrid() { + return grid; + } + + /** + * Gets the active cell for this event. + * + * @return active cell + */ + public Cell getActiveCell() { + return activeCell; + } + + @Override + protected void dispatch(HANDLER handler) { + activeCell = grid.activeCellHandler.getActiveCell(); + activeSection = GridSection.FOOTER; + final RowContainer container = grid.activeCellHandler.container; + if (container == grid.escalator.getHeader()) { + activeSection = GridSection.HEADER; + } else if (container == grid.escalator.getBody()) { + activeSection = GridSection.BODY; + } + } + + @Override + public Type<HANDLER> getAssociatedType() { + return associatedType; + } + } + + private GridKeyDownEvent<T> keyDown = new GridKeyDownEvent<T>(this); + private GridKeyUpEvent<T> keyUp = new GridKeyUpEvent<T>(this); + private GridKeyPressEvent<T> keyPress = new GridKeyPressEvent<T>(this); + + private class ActiveCellHandler { + + private RowContainer container = escalator.getBody(); + private int activeRow = 0; + private int activeColumn = 0; + private int lastActiveBodyRow = 0; + private int lastActiveHeaderRow = 0; + private int lastActiveFooterRow = 0; + private TableCellElement cellWithActiveStyle = null; + private TableRowElement rowWithActiveStyle = null; + + public ActiveCellHandler() { + sinkEvents(getNavigationEvents()); + } + + private Cell getActiveCell() { + return new Cell(activeRow, activeColumn, cellWithActiveStyle); + } + + /** + * Sets style names for given cell when needed. + */ + public void updateActiveCellStyle(FlyweightCell cell, + RowContainer cellContainer) { + int cellRow = cell.getRow(); + int cellColumn = cell.getColumn(); + int colSpan = cell.getColSpan(); + boolean columnActive = Range.withLength(cellColumn, colSpan) + .contains(activeColumn); + + if (cellContainer == container) { + // Cell is in the current container + if (cellRow == activeRow && columnActive) { + if (cellWithActiveStyle != cell.getElement()) { + // Cell is correct but it does not have active style + if (cellWithActiveStyle != null) { + // Remove old active style + setStyleName(cellWithActiveStyle, + cellActiveStyleName, false); + } + cellWithActiveStyle = cell.getElement(); + // Add active style to correct cell. + setStyleName(cellWithActiveStyle, cellActiveStyleName, + true); + } + } else if (cellWithActiveStyle == cell.getElement()) { + // Due to escalator reusing cells, a new cell has the same + // element but is not the active cell. + setStyleName(cellWithActiveStyle, cellActiveStyleName, + false); + cellWithActiveStyle = null; + } + } + + if (cellContainer == escalator.getHeader() + || cellContainer == escalator.getFooter()) { + // Correct header and footer column also needs highlighting + setStyleName(cell.getElement(), headerFooterActiveStyleName, + columnActive); + } + } + + /** + * Sets active row style name for given row if needed. + * + * @param row + * a row object + */ + public void updateActiveRowStyle(Row row) { + if (activeRow == row.getRow() && container == escalator.getBody()) { + if (row.getElement() != rowWithActiveStyle) { + // Row should have active style but does not have it. + if (rowWithActiveStyle != null) { + setStyleName(rowWithActiveStyle, rowActiveStyleName, + false); + } + rowWithActiveStyle = row.getElement(); + setStyleName(rowWithActiveStyle, rowActiveStyleName, true); + } + } else if (rowWithActiveStyle == row.getElement() + || (container != escalator.getBody() && rowWithActiveStyle != null)) { + // Remove active style. + setStyleName(rowWithActiveStyle, rowActiveStyleName, false); + rowWithActiveStyle = null; + } + } + + /** + * Sets currently active cell to a cell in given container with given + * indices. + * + * @param row + * new active row + * @param column + * new active column + * @param container + * new container + */ + private void setActiveCell(int row, int column, RowContainer container) { + if (row == activeRow && column == activeColumn + && container == this.container) { + return; + } + + int oldRow = activeRow; + int oldColumn = activeColumn; + activeRow = row; + activeColumn = column; + + if (container == escalator.getBody()) { + scrollToRow(activeRow); + } + escalator.scrollToColumn(activeColumn, ScrollDestination.ANY, 10); + + if (this.container == container) { + if (container != escalator.getBody()) { + if (oldColumn == activeColumn && oldRow != activeRow) { + refreshRow(oldRow); + } else if (oldColumn != activeColumn) { + refreshHeader(); + refreshFooter(); + } + } else { + if (oldRow != activeRow) { + refreshRow(oldRow); + } + + if (oldColumn != activeColumn) { + refreshHeader(); + refreshFooter(); + } + } + } else { + RowContainer oldContainer = this.container; + this.container = container; + + if (oldContainer == escalator.getBody()) { + lastActiveBodyRow = oldRow; + } else if (oldContainer == escalator.getHeader()) { + lastActiveHeaderRow = oldRow; + } else { + lastActiveFooterRow = oldRow; + } + + if (oldColumn != activeColumn) { + refreshHeader(); + refreshFooter(); + if (oldContainer == escalator.getBody()) { + oldContainer.refreshRows(oldRow, 1); + } + } else { + oldContainer.refreshRows(oldRow, 1); + } + } + refreshRow(activeRow); + } + + /** + * Sets currently active cell used for keyboard navigation. Note that + * active cell is not JavaScript {@code document.activeElement}. + * + * @param cell + * a cell object + */ + public void setActiveCell(Cell cell) { + setActiveCell(cell.getRow(), cell.getColumn(), + escalator.findRowContainer(cell.getElement())); + } + + /** + * Gets list of events that can be used for active cell navigation. + * + * @return list of navigation related event types + */ + public Collection<String> getNavigationEvents() { + return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK); + } + + /** + * Handle events that can change the currently active cell. + */ + public void handleNavigationEvent(Event event, Cell cell) { + if (event.getType().equals(BrowserEvents.CLICK) && cell != null) { + setActiveCell(cell); + getElement().focus(); + } else if (event.getType().equals(BrowserEvents.KEYDOWN)) { + int newRow = activeRow; + int newColumn = activeColumn; + RowContainer newContainer = container; + + switch (event.getKeyCode()) { + case KeyCodes.KEY_DOWN: + newRow += 1; + break; + case KeyCodes.KEY_UP: + newRow -= 1; + break; + case KeyCodes.KEY_RIGHT: + newColumn += 1; + break; + case KeyCodes.KEY_LEFT: + newColumn -= 1; + break; + case KeyCodes.KEY_TAB: + if (event.getShiftKey()) { + newContainer = getPreviousContainer(container); + } else { + newContainer = getNextContainer(container); + } + + if (newContainer == container) { + return; + } + break; + default: + return; + } + + if (newContainer != container) { + if (newContainer == escalator.getBody()) { + newRow = lastActiveBodyRow; + } else if (newContainer == escalator.getHeader()) { + newRow = lastActiveHeaderRow; + } else { + newRow = lastActiveFooterRow; + } + } else if (newRow < 0) { + newContainer = getPreviousContainer(newContainer); + + if (newContainer == container) { + newRow = 0; + } else if (newContainer == escalator.getBody()) { + newRow = getLastVisibleRowIndex(); + } else { + newRow = newContainer.getRowCount() - 1; + } + } else if (newRow >= container.getRowCount()) { + newContainer = getNextContainer(newContainer); + + if (newContainer == container) { + newRow = container.getRowCount() - 1; + } else if (newContainer == escalator.getBody()) { + newRow = getFirstVisibleRowIndex(); + } else { + newRow = 0; + } + } + + if (newContainer.getRowCount() == 0) { + // There are no rows in the container. Can't change the + // active cell. + return; + } + + if (newColumn < 0) { + newColumn = 0; + } else if (newColumn >= getColumnCount()) { + newColumn = getColumnCount() - 1; + } + + event.preventDefault(); + event.stopPropagation(); + + setActiveCell(newRow, newColumn, newContainer); + } + + } + + private RowContainer getPreviousContainer(RowContainer current) { + if (current == escalator.getFooter()) { + current = escalator.getBody(); + } else if (current == escalator.getBody()) { + current = escalator.getHeader(); + } else { + return current; + } + + if (current.getRowCount() == 0) { + return getPreviousContainer(current); + } + return current; + } + + private RowContainer getNextContainer(RowContainer current) { + if (current == escalator.getHeader()) { + current = escalator.getBody(); + } else if (current == escalator.getBody()) { + current = escalator.getFooter(); + } else { + return current; + } + + if (current.getRowCount() == 0) { + return getNextContainer(current); + } + return current; + } + + private void refreshRow(int row) { + container.refreshRows(row, 1); + } + } + + private class SelectionColumn extends GridColumn<Boolean, T> { + private boolean initDone = false; + + public SelectionColumn(final Renderer<Boolean> selectColumnRenderer) { + super(selectColumnRenderer); + } + + public void initDone() { + initDone = true; + } + + @Override + public void setVisible(boolean visible) { + if (!visible && initDone) { + throw new UnsupportedOperationException("The selection " + + "column cannot be modified after init"); + } else { + super.setVisible(visible); + } + } + + @Override + public void setWidth(int pixels) { + if (pixels != getWidth() && initDone) { + throw new UnsupportedOperationException("The selection " + + "column cannot be modified after init"); + } else { + super.setWidth(pixels); + } + } + + @Override + public Boolean getValue(T row) { + return Boolean.valueOf(isSelected(row)); + } + } + + /** + * Class for sorting at a later time + */ + private class LazySorter extends Timer { + + private Cell cell; + + private boolean multisort; + + @Override + public void run() { + SortOrder sortingOrder = getSortOrder(getColumnFromVisibleIndex(cell + .getColumn())); + if (sortingOrder == null) { + /* + * No previous sorting, sort Ascending + */ + sort(cell, SortDirection.ASCENDING, multisort); + + } else { + // Toggle sorting + SortDirection direction = sortingOrder.getDirection(); + if (direction == SortDirection.ASCENDING) { + sort(cell, SortDirection.DESCENDING, multisort); + } else { + sort(cell, SortDirection.ASCENDING, multisort); + } + } + } + + /** + * Set the cell reference to the primary cell that sorting should be + * done for. + * + * @param cell + * + */ + public void setCellReference(Cell cell) { + this.cell = cell; + } + + /** + * Is multiple column sorting is enabled/disabled + * + * @param multisort + * true if multiple column sorting is enabled + */ + public void setMultisort(boolean multisort) { + this.multisort = multisort; + } + + /** + * Sorts the column in a direction + */ + private void sort(Cell cell, SortDirection direction, boolean multisort) { + TableCellElement th = TableCellElement.as(cell.getElement()); + + // Apply primary sorting on clicked column + GridColumn<?, ?> columnInstance = getColumnFromVisibleIndex(cell + .getColumn()); + Sort sorting = Sort.by(columnInstance, direction); + + // Re-apply old sorting to the sort order + if (multisort) { + for (SortOrder order : getSortOrder()) { + if (order.getColumn() != columnInstance) { + sorting = sorting.then(order.getColumn(), + order.getDirection()); + } + } + } + + // Perform sorting; indicate originator as user + Grid.this.setSortOrder(sorting.build(), SortEventOriginator.USER); + } + } + + /** + * Escalator used internally by grid to render the rows + */ + private Escalator escalator = GWT.create(Escalator.class); + + private final GridHeader header = GWT.create(GridHeader.class); + + private final GridFooter footer = GWT.create(GridFooter.class); + + /** + * List of columns in the grid. Order defines the visible order. + */ + private final List<GridColumn<?, T>> columns = new ArrayList<GridColumn<?, T>>(); + + /** + * The datasource currently in use. <em>Note:</em> it is <code>null</code> + * on initialization, but not after that. + */ + private DataSource<T> dataSource; + + /** + * Currently available row range in DataSource. + */ + private Range currentDataAvailable = Range.withLength(0, 0); + + /** + * The last column frozen counter from the left + */ + private GridColumn<?, T> lastFrozenColumn; + + /** + * Current sort order. The (private) sort() method reads this list to + * determine the order in which to present rows. + */ + private List<SortOrder> sortOrder = new ArrayList<SortOrder>(); + + private Renderer<Boolean> selectColumnRenderer = null; + + private SelectionColumn selectionColumn; + + private String rowHasDataStyleName; + private String rowSelectedStyleName; + private String cellActiveStyleName; + private String rowActiveStyleName; + private String headerFooterActiveStyleName; + + /** + * Current selection model. + */ + private SelectionModel<T> selectionModel; + + protected final ActiveCellHandler activeCellHandler; + + private final LazySorter lazySorter = new LazySorter(); + + /** + * Enumeration for easy setting of selection mode. + */ + public enum SelectionMode { + + /** + * Shortcut for {@link SelectionModelSingle}. + */ + SINGLE { + + @Override + protected <T> SelectionModel<T> createModel() { + return new SelectionModelSingle<T>(); + } + }, + + /** + * Shortcut for {@link SelectionModelMulti}. + */ + MULTI { + + @Override + protected <T> SelectionModel<T> createModel() { + return new SelectionModelMulti<T>(); + } + }, + + /** + * Shortcut for {@link SelectionModelNone}. + */ + NONE { + + @Override + protected <T> SelectionModel<T> createModel() { + return new SelectionModelNone<T>(); + } + }; + + protected abstract <T> SelectionModel<T> createModel(); + } + + /** + * Base class for grid columns internally used by the Grid. The user should + * use {@link GridColumn} when creating new columns. + * + * @param <C> + * the column type + * + * @param <T> + * the row type + */ + static abstract class AbstractGridColumn<C, T> implements HasVisibility { + + /** + * the column is associated with + */ + private Grid<T> grid; + + /** + * Should the column be visible in the grid + */ + private boolean visible = true; + + /** + * Width of column in pixels + */ + private int width = 100; + + /** + * Renderer for rendering a value into the cell + */ + private Renderer<? super C> bodyRenderer; + + private boolean sortable = false; + + /** + * Constructs a new column with a custom renderer. + * + * @param renderer + * The renderer to use for rendering the cells + */ + public AbstractGridColumn(Renderer<? super C> renderer) { + if (renderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + bodyRenderer = renderer; + } + + /** + * Internally used by the grid to set itself + * + * @param grid + */ + private void setGrid(Grid<T> grid) { + if (this.grid != null && grid != null) { + // Trying to replace grid + throw new IllegalStateException( + "Column already is attached to grid. Remove the column first from the grid and then add it."); + } + + this.grid = grid; + } + + /** + * Is the column visible. By default all columns are visible. + * + * @return <code>true</code> if the column is visible + */ + @Override + public boolean isVisible() { + return visible; + } + + /** + * Sets a column as visible in the grid. + * + * @param visible + * <code>true</code> if the column should be displayed in the + * grid + */ + @Override + public void setVisible(boolean visible) { + if (this.visible == visible) { + return; + } + + this.visible = visible; + + if (grid != null) { + int index = findIndexOfColumn(); + ColumnConfiguration conf = grid.escalator + .getColumnConfiguration(); + + if (visible) { + conf.insertColumns(index, 1); + } else { + conf.removeColumns(index, 1); + } + + for (HeaderRow row : grid.getHeader().getRows()) { + row.calculateColspans(); + } + + for (FooterRow row : grid.getFooter().getRows()) { + row.calculateColspans(); + } + } + } + + /** + * Returns the data that should be rendered into the cell. By default + * returning Strings and Widgets are supported. If the return type is a + * String then it will be treated as preformatted text. + * <p> + * To support other types you will need to pass a custom renderer to the + * column via the column constructor. + * + * @param row + * The row object that provides the cell content. + * + * @return The cell content + */ + public abstract C getValue(T row); + + /** + * The renderer to render the cell width. By default renders the data as + * a String or adds the widget into the cell if the column type is of + * widget type. + * + * @return The renderer to render the cell content with + */ + public Renderer<? super C> getRenderer() { + return bodyRenderer; + } + + /** + * Finds the index of this column instance + * + */ + private int findIndexOfColumn() { + return grid.findVisibleColumnIndex((GridColumn<?, T>) this); + } + + /** + * Sets the pixel width of the column. Use a negative value for the grid + * to autosize column based on content and available space + * + * @param pixels + * the width in pixels or negative for auto sizing + */ + public void setWidth(int pixels) { + width = pixels; + + if (grid != null && isVisible()) { + int index = findIndexOfColumn(); + ColumnConfiguration conf = grid.escalator + .getColumnConfiguration(); + conf.setColumnWidth(index, pixels); + } + } + + /** + * Returns the pixel width of the column + * + * @return pixel width of the column + */ + public int getWidth() { + return width; + } + + /** + * Enables sort indicators for the grid. + * <p> + * <b>Note:</b>The API can still sort the column even if this is set to + * <code>false</code>. + * + * @param sortable + * <code>true</code> when column sort indicators are visible. + */ + public void setSortable(boolean sortable) { + if (this.sortable != sortable) { + this.sortable = sortable; + grid.refreshHeader(); + } + } + + /** + * Are sort indicators shown for the column. + * + * @return <code>true</code> if the column is sortable + */ + public boolean isSortable() { + return sortable; + } + } + + protected class BodyUpdater implements EscalatorUpdater { + + @Override + public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) { + for (FlyweightCell cell : cellsToAttach) { + Renderer<?> renderer = findRenderer(cell); + if (renderer instanceof ComplexRenderer) { + ((ComplexRenderer<?>) renderer).init(cell); + } + } + } + + @Override + public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) { + for (FlyweightCell cell : attachedCells) { + Renderer<?> renderer = findRenderer(cell); + if (renderer instanceof WidgetRenderer) { + WidgetRenderer<?, ?> widgetRenderer = (WidgetRenderer<?, ?>) renderer; + + Widget widget = widgetRenderer.createWidget(); + assert widget != null : "WidgetRenderer.createWidget() returned null. It should return a widget."; + assert widget.getParent() == null : "WidgetRenderer.createWidget() returned a widget which already is attached."; + assert cell.getElement().getChildCount() == 0 : "Cell content should be empty when adding Widget"; + + // Physical attach + cell.getElement().appendChild(widget.getElement()); + + // Logical attach + setParent(widget, Grid.this); + } + } + } + + @Override + public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) { + int rowIndex = row.getRow(); + Element rowElement = row.getElement(); + T rowData = dataSource.getRow(rowIndex); + + boolean hasData = rowData != null; + + // Assign stylename for rows with data + boolean usedToHaveData = rowElement + .hasClassName(rowHasDataStyleName); + + if (usedToHaveData != hasData) { + setStyleName(rowElement, rowHasDataStyleName, hasData); + } + + // Assign stylename for selected rows + if (hasData) { + setStyleName(rowElement, rowSelectedStyleName, + isSelected(rowData)); + } else if (usedToHaveData) { + setStyleName(rowElement, rowSelectedStyleName, false); + } + + activeCellHandler.updateActiveRowStyle(row); + + for (FlyweightCell cell : cellsToUpdate) { + GridColumn<?, T> column = getColumnFromVisibleIndex(cell + .getColumn()); + + assert column != null : "Column was not found from cell (" + + cell.getColumn() + "," + cell.getRow() + ")"; + + activeCellHandler.updateActiveCellStyle(cell, + escalator.getBody()); + + Renderer renderer = column.getRenderer(); + + // Hide cell content if needed + if (renderer instanceof ComplexRenderer) { + ComplexRenderer clxRenderer = (ComplexRenderer) renderer; + if (hasData) { + if (!usedToHaveData) { + // Prepare cell for rendering + clxRenderer.setContentVisible(cell, true); + } + + Object value = column.getValue(rowData); + clxRenderer.render(cell, value); + + } else { + // Prepare cell for no data + clxRenderer.setContentVisible(cell, false); + } + + } else if (hasData) { + // Simple renderers just render + Object value = column.getValue(rowData); + renderer.render(cell, value); + + } else { + // Clear cell if there is no data + cell.getElement().removeAllChildren(); + } + } + } + + @Override + public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) { + for (FlyweightCell cell : cellsToDetach) { + Renderer renderer = findRenderer(cell); + if (renderer instanceof WidgetRenderer) { + Widget w = Util.findWidget(cell.getElement() + .getFirstChildElement(), Widget.class); + if (w != null) { + + // Logical detach + setParent(w, null); + + // Physical detach + cell.getElement().removeChild(w.getElement()); + } + } + } + } + + @Override + public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) { + for (FlyweightCell cell : detachedCells) { + Renderer renderer = findRenderer(cell); + if (renderer instanceof ComplexRenderer) { + ((ComplexRenderer) renderer).destroy(cell); + } + } + } + } + + protected class StaticSectionUpdater implements EscalatorUpdater { + + private GridStaticSection<?> section; + private RowContainer container; + + public StaticSectionUpdater(GridStaticSection<?> section, + RowContainer container) { + super(); + this.section = section; + this.container = container; + } + + @Override + public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) { + GridStaticSection.StaticRow<?> staticRow = section.getRow(row + .getRow()); + + final List<Integer> columnIndices = getVisibleColumnIndices(); + + for (FlyweightCell cell : cellsToUpdate) { + + int index = columnIndices.get(cell.getColumn()); + final StaticCell metadata = staticRow.getCell(index); + + // Decorate default row with sorting indicators + if (staticRow instanceof HeaderRow) { + addSortingIndicatorsToHeaderRow((HeaderRow) staticRow, cell); + } + + // Assign colspan to cell before rendering + cell.setColSpan(metadata.getColspan()); + + switch (metadata.getType()) { + case TEXT: + cell.getElement().setInnerText(metadata.getText()); + break; + case HTML: + cell.getElement().setInnerHTML(metadata.getHtml()); + break; + case WIDGET: + preDetach(row, Arrays.asList(cell)); + cell.getElement().setInnerHTML(""); + postAttach(row, Arrays.asList(cell)); + break; + } + + activeCellHandler.updateActiveCellStyle(cell, container); + } + } + + private void addSortingIndicatorsToHeaderRow(HeaderRow headerRow, + FlyweightCell cell) { + + cleanup(cell); + + GridColumn<?, ?> column = getColumnFromVisibleIndex(cell + .getColumn()); + SortOrder sortingOrder = getSortOrder(column); + if (!headerRow.isDefault() || !column.isSortable() + || sortingOrder == null) { + // Only apply sorting indicators to sortable header columns in + // the default header row + return; + } + + Element cellElement = cell.getElement(); + + if (SortDirection.ASCENDING == sortingOrder.getDirection()) { + cellElement.addClassName("sort-asc"); + } else { + cellElement.addClassName("sort-desc"); + } + + int sortIndex = Grid.this.getSortOrder().indexOf(sortingOrder); + if (sortIndex > -1 && Grid.this.getSortOrder().size() > 1) { + // Show sort order indicator if column is + // sorted and other sorted columns also exists. + cellElement.setAttribute("sort-order", + String.valueOf(sortIndex + 1)); + } + } + + /** + * Finds the sort order for this column + */ + private SortOrder getSortOrder(GridColumn<?, ?> column) { + for (SortOrder order : Grid.this.getSortOrder()) { + if (order.getColumn() == column) { + return order; + } + } + return null; + } + + private void cleanup(FlyweightCell cell) { + Element cellElement = cell.getElement(); + cellElement.removeAttribute("sort-order"); + cellElement.removeClassName("sort-desc"); + cellElement.removeClassName("sort-asc"); + } + + @Override + public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) { + } + + @Override + public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) { + GridStaticSection.StaticRow<?> gridRow = section.getRow(row + .getRow()); + List<Integer> columnIndices = getVisibleColumnIndices(); + + for (FlyweightCell cell : attachedCells) { + int index = columnIndices.get(cell.getColumn()); + StaticCell metadata = gridRow.getCell(index); + /* + * If the cell contains widgets that are not currently attach + * then attach them now. + */ + if (GridStaticCellType.WIDGET.equals(metadata.getType())) { + final Widget widget = metadata.getWidget(); + final Element cellElement = cell.getElement(); + + if (!widget.isAttached()) { + + // Physical attach + cellElement.appendChild(widget.getElement()); + + // Logical attach + setParent(widget, Grid.this); + + getLogger().info("Attached widget " + widget); + } + } + } + } + + @Override + public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) { + if (section.getRowCount() > row.getRow()) { + GridStaticSection.StaticRow<?> gridRow = section.getRow(row + .getRow()); + List<Integer> columnIndices = getVisibleColumnIndices(); + for (FlyweightCell cell : cellsToDetach) { + int index = columnIndices.get(cell.getColumn()); + StaticCell metadata = gridRow.getCell(index); + + if (GridStaticCellType.WIDGET.equals(metadata.getType()) + && metadata.getWidget().isAttached()) { + + Widget widget = metadata.getWidget(); + + // Logical detach + setParent(widget, null); + + // Physical detach + widget.getElement().removeFromParent(); + + getLogger().info("Detached widget " + widget); + } + } + } + } + + @Override + public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) { + } + + private List<Integer> getVisibleColumnIndices() { + List<Integer> indices = new ArrayList<Integer>(getColumnCount()); + for (int i = 0; i < getColumnCount(); i++) { + if (getColumn(i).isVisible()) { + indices.add(i); + } + } + return indices; + } + }; + + /** + * Creates a new instance. + */ + public Grid() { + initWidget(escalator); + getElement().setTabIndex(0); + activeCellHandler = new ActiveCellHandler(); + + setStylePrimaryName("v-grid"); + + escalator.getHeader().setEscalatorUpdater(createHeaderUpdater()); + escalator.getBody().setEscalatorUpdater(createBodyUpdater()); + escalator.getFooter().setEscalatorUpdater(createFooterUpdater()); + + header.setGrid(this); + HeaderRow defaultRow = header.appendRow(); + header.setDefaultRow(defaultRow); + + footer.setGrid(this); + + setSelectionMode(SelectionMode.SINGLE); + + escalator + .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + if (dataSource != null) { + dataSource.ensureAvailability( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + } + }); + + // Default action on SelectionChangeEvents. Refresh the body so changed + // become visible. + addSelectionChangeHandler(new SelectionChangeHandler<T>() { + + @Override + public void onSelectionChange(SelectionChangeEvent<T> event) { + refreshBody(); + } + }); + + // Sink header events and key events + sinkEvents(getHeader().getConsumedEvents()); + sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP, + BrowserEvents.KEYPRESS)); + } + + @Override + public void setStylePrimaryName(String style) { + super.setStylePrimaryName(style); + escalator.setStylePrimaryName(style); + rowHasDataStyleName = getStylePrimaryName() + "-row-has-data"; + rowSelectedStyleName = getStylePrimaryName() + "-row-selected"; + cellActiveStyleName = getStylePrimaryName() + "-cell-active"; + headerFooterActiveStyleName = getStylePrimaryName() + "-header-active"; + rowActiveStyleName = getStylePrimaryName() + "-row-active"; + + if (isAttached()) { + refreshHeader(); + refreshBody(); + refreshFooter(); + } + } + + /** + * Creates the escalator updater used to update the header rows in this + * grid. The updater is invoked when header rows or columns are added or + * removed, or the content of existing header cells is changed. + * + * @return the new header updater instance + * + * @see GridHeader + * @see Grid#getHeader() + */ + protected EscalatorUpdater createHeaderUpdater() { + return new StaticSectionUpdater(header, escalator.getHeader()); + } + + /** + * Creates the escalator updater used to update the body rows in this grid. + * The updater is invoked when body rows or columns are added or removed, + * the content of body cells is changed, or the body is scrolled to expose + * previously hidden content. + * + * @return the new body updater instance + */ + protected EscalatorUpdater createBodyUpdater() { + return new BodyUpdater(); + } + + /** + * Creates the escalator updater used to update the footer rows in this + * grid. The updater is invoked when header rows or columns are added or + * removed, or the content of existing header cells is changed. + * + * @return the new footer updater instance + * + * @see GridFooter + * @see #getFooter() + */ + protected EscalatorUpdater createFooterUpdater() { + return new StaticSectionUpdater(footer, escalator.getFooter()); + } + + /** + * Refreshes header or footer rows on demand + * + * @param rows + * The row container + * @param firstRowIsVisible + * is the first row visible + * @param isHeader + * <code>true</code> if we refreshing the header, else assumed + * the footer + */ + private void refreshRowContainer(RowContainer rows, + GridStaticSection<?> section) { + + // Add or Remove rows on demand + int rowDiff = section.getVisibleRowCount() - rows.getRowCount(); + if (rowDiff > 0) { + rows.insertRows(0, rowDiff); + } else if (rowDiff < 0) { + rows.removeRows(0, -rowDiff); + } + + // Refresh all the rows + if (rows.getRowCount() > 0) { + rows.refreshRows(0, rows.getRowCount()); + } + } + + /** + * Refreshes all header rows + */ + void refreshHeader() { + refreshRowContainer(escalator.getHeader(), header); + } + + /** + * Refreshes all body rows + */ + private void refreshBody() { + escalator.getBody().refreshRows(0, escalator.getBody().getRowCount()); + } + + /** + * Refreshes all footer rows + */ + void refreshFooter() { + refreshRowContainer(escalator.getFooter(), footer); + } + + /** + * Adds a column as the last column in the grid. + * + * @param column + * the column to add + */ + public void addColumn(GridColumn<?, T> column) { + addColumn(column, getColumnCount()); + } + + /** + * Inserts a column into a specific position in the grid. + * + * @param index + * the index where the column should be inserted into + * @param column + * the column to add + * @throws IllegalStateException + * if Grid's current selection model renders a selection column, + * and {@code index} is 0. + */ + public void addColumn(GridColumn<?, T> column, int index) { + if (column == selectionColumn) { + throw new IllegalArgumentException("The selection column many " + + "not be added manually"); + } else if (selectionColumn != null && index == 0) { + throw new IllegalStateException("A column cannot be inserted " + + "before the selection column"); + } + + addColumnSkipSelectionColumnCheck(column, index); + } + + private void addColumnSkipSelectionColumnCheck(GridColumn<?, T> column, + int index) { + // Register column with grid + columns.add(index, column); + + header.addColumn(column, index); + footer.addColumn(column, index); + + // Register this grid instance with the column + ((AbstractGridColumn<?, T>) column).setGrid(this); + + // Insert column into escalator + if (column.isVisible()) { + int visibleIndex = findVisibleColumnIndex(column); + ColumnConfiguration conf = escalator.getColumnConfiguration(); + + // Insert column + conf.insertColumns(visibleIndex, 1); + + // Transfer column width from column object to escalator + conf.setColumnWidth(visibleIndex, column.getWidth()); + } + + if (lastFrozenColumn != null + && ((AbstractGridColumn<?, T>) lastFrozenColumn) + .findIndexOfColumn() < index) { + refreshFrozenColumns(); + } + + // Sink all renderer events + Set<String> events = new HashSet<String>(); + events.addAll(getConsumedEventsForRenderer(column.getRenderer())); + + sinkEvents(events); + } + + private void sinkEvents(Collection<String> events) { + assert events != null; + + int eventsToSink = 0; + for (String typeName : events) { + int typeInt = Event.getTypeInt(typeName); + if (typeInt < 0) { + // Type not recognized by typeInt + sinkBitlessEvent(typeName); + } else { + eventsToSink |= typeInt; + } + } + + if (eventsToSink > 0) { + sinkEvents(eventsToSink); + } + } + + private int findVisibleColumnIndex(GridColumn<?, T> column) { + int idx = 0; + for (GridColumn<?, T> c : columns) { + if (c == column) { + return idx; + } else if (c.isVisible()) { + idx++; + } + } + return -1; + } + + private GridColumn<?, T> getColumnFromVisibleIndex(int index) { + int idx = -1; + for (GridColumn<?, T> c : columns) { + if (c.isVisible()) { + idx++; + } + if (index == idx) { + return c; + } + } + return null; + } + + private Renderer<?> findRenderer(FlyweightCell cell) { + GridColumn<?, T> column = getColumnFromVisibleIndex(cell.getColumn()); + assert column != null : "Could not find column at index:" + + cell.getColumn(); + return column.getRenderer(); + } + + /** + * Removes a column from the grid. + * + * @param column + * the column to remove + */ + public void removeColumn(GridColumn<?, T> column) { + if (column != null && column.equals(selectionColumn)) { + throw new IllegalArgumentException( + "The selection column may not be removed manually."); + } + + removeColumnSkipSelectionColumnCheck(column); + } + + private void removeColumnSkipSelectionColumnCheck(GridColumn<?, T> column) { + int columnIndex = columns.indexOf(column); + int visibleIndex = findVisibleColumnIndex(column); + columns.remove(columnIndex); + + header.removeColumn(columnIndex); + footer.removeColumn(columnIndex); + + // de-register column with grid + ((AbstractGridColumn<?, T>) column).setGrid(null); + + if (column.isVisible()) { + ColumnConfiguration conf = escalator.getColumnConfiguration(); + conf.removeColumns(visibleIndex, 1); + } + + if (column.equals(lastFrozenColumn)) { + setLastFrozenColumn(null); + } else { + refreshFrozenColumns(); + } + } + + /** + * Returns the amount of columns in the grid. + * + * @return The number of columns in the grid + */ + public int getColumnCount() { + return columns.size(); + } + + /** + * Returns a list of columns in the grid. + * + * @return A unmodifiable list of the columns in the grid + */ + public List<GridColumn<?, T>> getColumns() { + return Collections.unmodifiableList(new ArrayList<GridColumn<?, T>>( + columns)); + } + + /** + * Returns a column by its index in the grid. + * + * @param index + * the index of the column + * @return The column in the given index + * @throws IllegalArgumentException + * if the column index does not exist in the grid + */ + public GridColumn<?, T> getColumn(int index) + throws IllegalArgumentException { + if (index < 0 || index >= columns.size()) { + throw new IllegalStateException("Column not found."); + } + return columns.get(index); + } + + /** + * Returns the header section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the header + */ + public GridHeader getHeader() { + return header; + } + + /** + * Returns the footer section of this grid. The default footer is empty. + * + * @return the footer + */ + public GridFooter getFooter() { + return footer; + } + + /** + * {@inheritDoc} + * <p> + * <em>Note:</em> This method will change the widget's size in the browser + * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. + * + * @see #setHeightMode(HeightMode) + */ + @Override + public void setHeight(String height) { + escalator.setHeight(height); + } + + @Override + public void setWidth(String width) { + escalator.setWidth(width); + } + + /** + * Sets the data source used by this grid. + * + * @param dataSource + * the data source to use, not null + * @throws IllegalArgumentException + * if <code>dataSource</code> is <code>null</code> + */ + public void setDataSource(DataSource<T> dataSource) + throws IllegalArgumentException { + if (dataSource == null) { + throw new IllegalArgumentException("dataSource can't be null."); + } + + selectionModel.reset(); + + if (this.dataSource != null) { + this.dataSource.setDataChangeHandler(null); + } + + this.dataSource = dataSource; + dataSource.setDataChangeHandler(new DataChangeHandler() { + @Override + public void dataUpdated(int firstIndex, int numberOfItems) { + escalator.getBody().refreshRows(firstIndex, numberOfItems); + } + + @Override + public void dataRemoved(int firstIndex, int numberOfItems) { + escalator.getBody().removeRows(firstIndex, numberOfItems); + } + + @Override + public void dataAdded(int firstIndex, int numberOfItems) { + escalator.getBody().insertRows(firstIndex, numberOfItems); + } + + @Override + public void dataAvailable(int firstIndex, int numberOfItems) { + currentDataAvailable = Range.withLength(firstIndex, + numberOfItems); + fireEvent(new DataAvailableEvent(currentDataAvailable)); + } + }); + + int previousRowCount = escalator.getBody().getRowCount(); + if (previousRowCount != 0) { + escalator.getBody().removeRows(0, previousRowCount); + } + + int estimatedSize = dataSource.getEstimatedSize(); + if (estimatedSize > 0) { + escalator.getBody().insertRows(0, estimatedSize); + } + + } + + /** + * Gets the {@Link DataSource} for this Grid. + * + * @return the data source used by this grid + */ + public DataSource<T> getDataSource() { + return dataSource; + } + + /** + * Sets the rightmost frozen column in the grid. + * <p> + * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or <code>null</code> to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenColumn(GridColumn<?, T> lastFrozenColumn) { + this.lastFrozenColumn = lastFrozenColumn; + refreshFrozenColumns(); + } + + private void refreshFrozenColumns() { + final int frozenCount; + if (lastFrozenColumn != null) { + frozenCount = columns.indexOf(lastFrozenColumn) + 1; + if (frozenCount == 0) { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } else { + frozenCount = 0; + } + + escalator.getColumnConfiguration().setFrozenColumnCount(frozenCount); + } + + /** + * Gets the rightmost frozen column in the grid. + * <p> + * <em>Note:</em> Most usually, this method returns the very value set with + * {@link #setLastFrozenColumn(GridColumn)}. This value, however, can be + * reset to <code>null</code> if the column is removed from this grid. + * + * @return the rightmost frozen column in the grid, or <code>null</code> if + * no columns are frozen. + */ + public GridColumn<?, T> getLastFrozenColumn() { + return lastFrozenColumn; + } + + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler handler) { + /* + * Reusing Escalator's RowVisibilityChangeHandler, since a scroll + * concept is too abstract. e.g. the event needs to be re-sent when the + * widget is resized. + */ + return escalator.addRowVisibilityChangeHandler(handler); + } + + /** + * Scrolls to a certain row, using {@link ScrollDestination#ANY}. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @throws IllegalArgumentException + * if rowIndex is below zero, or above the maximum value + * supported by the data source. + */ + public void scrollToRow(int rowIndex) throws IllegalArgumentException { + scrollToRow(rowIndex, ScrollDestination.ANY, + GridConstants.DEFAULT_PADDING); + } + + /** + * Scrolls to a certain row, using user-specified scroll destination. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @param destination + * desired destination placement of scrolled-to-row. See + * {@link ScrollDestination} for more information. + * @throws IllegalArgumentException + * if rowIndex is below zero, or above the maximum value + * supported by the data source. + */ + public void scrollToRow(int rowIndex, ScrollDestination destination) + throws IllegalArgumentException { + scrollToRow(rowIndex, destination, + destination == ScrollDestination.MIDDLE ? 0 + : GridConstants.DEFAULT_PADDING); + } + + /** + * Scrolls to a certain row using only user-specified parameters. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @param destination + * desired destination placement of scrolled-to-row. See + * {@link ScrollDestination} for more information. + * @param paddingPx + * number of pixels to overscroll. Behavior depends on + * destination. + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered row is undefined behavior, or if rowIndex is below + * zero or above the row count of the data source. + */ + private void scrollToRow(int rowIndex, ScrollDestination destination, + int paddingPx) throws IllegalArgumentException { + int maxsize = escalator.getBody().getRowCount() - 1; + + if (rowIndex < 0) { + throw new IllegalArgumentException("Row index (" + rowIndex + + ") is below zero!"); + } + + if (rowIndex > maxsize) { + throw new IllegalArgumentException("Row index (" + rowIndex + + ") is above maximum (" + maxsize + ")!"); + } + + escalator.scrollToRow(rowIndex, destination, paddingPx); + } + + /** + * Scrolls to the beginning of the very first row. + */ + public void scrollToStart() { + scrollToRow(0, ScrollDestination.START); + } + + /** + * Scrolls to the end of the very last row. + */ + public void scrollToEnd() { + scrollToRow(escalator.getBody().getRowCount() - 1, + ScrollDestination.END); + } + + /** + * Sets the vertical scroll offset. + * + * @param px + * the number of pixels this grid should be scrolled down + */ + public void setScrollTop(double px) { + escalator.setScrollTop(px); + } + + /** + * Gets the vertical scroll offset + * + * @return the number of pixels this grid is scrolled down + */ + public double getScrollTop() { + return escalator.getScrollTop(); + } + + private static final Logger getLogger() { + return Logger.getLogger(Grid.class.getName()); + } + + /** + * Sets the number of rows that should be visible in Grid's body, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * If Grid is currently not in {@link HeightMode#ROW}, the given value is + * remembered, and applied once the mode is applied. + * + * @param rows + * The height in terms of number of rows displayed in Grid's + * body. If Grid doesn't contain enough rows, white space is + * displayed instead. + * @throws IllegalArgumentException + * if {@code rows} is zero or less + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isInifinite(double) + * infinite} + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isNaN(double) NaN} + * + * @see #setHeightMode(HeightMode) + */ + public void setHeightByRows(double rows) throws IllegalArgumentException { + escalator.setHeightByRows(rows); + } + + /** + * Gets the amount of rows in Grid's body that are shown, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * By default, it is {@value Escalator#DEFAULT_HEIGHT_BY_ROWS}. + * + * @return the amount of rows that should be shown in Grid's body, while in + * {@link HeightMode#ROW}. + * @see #setHeightByRows(double) + */ + public double getHeightByRows() { + return escalator.getHeightByRows(); + } + + /** + * Defines the mode in which the Grid widget's height is calculated. + * <p> + * If {@link HeightMode#CSS} is given, Grid will respect the values given + * via {@link #setHeight(String)}, and behave as a traditional Widget. + * <p> + * If {@link HeightMode#ROW} is given, Grid will make sure that the body + * will display as many rows as {@link #getHeightByRows()} defines. + * <em>Note:</em> If headers/footers are inserted or removed, the widget + * will resize itself to still display the required amount of rows in its + * body. It also takes the horizontal scrollbar into account. + * + * @param heightMode + * the mode in to which Grid should be set + */ + public void setHeightMode(HeightMode heightMode) { + /* + * This method is a workaround for the fact that Vaadin re-applies + * widget dimensions (height/width) on each state change event. The + * original design was to have setHeight an setHeightByRow be equals, + * and whichever was called the latest was considered in effect. + * + * But, because of Vaadin always calling setHeight on the widget, this + * approach doesn't work. + */ + + escalator.setHeightMode(heightMode); + } + + /** + * Returns the current {@link HeightMode} the Grid is in. + * <p> + * Defaults to {@link HeightMode#CSS}. + * + * @return the current HeightMode + */ + public HeightMode getHeightMode() { + return escalator.getHeightMode(); + } + + private Set<String> getConsumedEventsForRenderer(Renderer<?> renderer) { + Set<String> events = new HashSet<String>(); + if (renderer instanceof ComplexRenderer) { + Collection<String> consumedEvents = ((ComplexRenderer<?>) renderer) + .getConsumedEvents(); + if (consumedEvents != null) { + events.addAll(consumedEvents); + } + } + return events; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + EventTarget target = event.getEventTarget(); + if (Element.is(target)) { + Element e = Element.as(target); + RowContainer container = escalator.findRowContainer(e); + Cell cell = null; + if (container != null) { + cell = container.getCell(e); + if (cell != null) { + // FIXME getFromVisibleIndex??? + GridColumn<?, T> gridColumn = columns.get(cell.getColumn()); + + if (container == escalator.getHeader()) { + if (getHeader().getRow(cell.getRow()).isDefault()) { + handleDefaultRowEvent(cell, event); + } + } else if (container == escalator.getFooter()) { + // NOP + } else if (gridColumn.getRenderer() instanceof ComplexRenderer) { + ComplexRenderer<?> cplxRenderer = (ComplexRenderer<?>) gridColumn + .getRenderer(); + if (cplxRenderer.getConsumedEvents().contains( + event.getType())) { + if (cplxRenderer.onBrowserEvent(cell, event)) { + return; + } + } + } + } + } + + Collection<String> navigation = activeCellHandler + .getNavigationEvents(); + if (navigation.contains(event.getType()) + && (Util.getFocusedElement() == getElement() || cell != null)) { + activeCellHandler.handleNavigationEvent(event, cell); + } + + handleGridNavigation(event, cell); + } + } + + private void handleGridNavigation(Event event, Cell cell) { + if (!event.getType().equals(BrowserEvents.KEYDOWN)) { + // Only handle key downs + return; + } + + int newRow = -1; + RowContainer container = escalator.getBody(); + switch (event.getKeyCode()) { + case KeyCodes.KEY_HOME: + if (container.getRowCount() > 0) { + newRow = 0; + } + break; + case KeyCodes.KEY_END: + if (container.getRowCount() > 0) { + newRow = container.getRowCount() - 1; + } + break; + case KeyCodes.KEY_PAGEUP: { + Range range = escalator.getVisibleRowRange(); + if (!range.isEmpty()) { + int firstIndex = getFirstVisibleRowIndex(); + newRow = firstIndex - range.length(); + if (newRow < 0) { + newRow = 0; + } + } + break; + } + case KeyCodes.KEY_PAGEDOWN: { + Range range = escalator.getVisibleRowRange(); + if (!range.isEmpty()) { + int lastIndex = getLastVisibleRowIndex(); + newRow = lastIndex + range.length(); + if (newRow >= container.getRowCount()) { + newRow = container.getRowCount() - 1; + } + } + break; + } + default: + return; + } + + scrollToRow(newRow); + } + + private Point rowEventTouchStartingPoint; + + private boolean handleDefaultRowEvent(final Cell cell, NativeEvent event) { + if (!getColumn(cell.getColumn()).isSortable()) { + // Only handle sorting events if the column is sortable + return false; + } + + if (BrowserEvents.TOUCHSTART.equals(event.getType())) { + if (event.getTouches().length() > 1) { + return false; + } + + event.preventDefault(); + + Touch touch = event.getChangedTouches().get(0); + rowEventTouchStartingPoint = new Point(touch.getClientX(), + touch.getClientY()); + + lazySorter.setCellReference(cell); + lazySorter.setMultisort(true); + lazySorter.schedule(GridConstants.LONG_TAP_DELAY); + + } else if (BrowserEvents.TOUCHMOVE.equals(event.getType())) { + if (event.getTouches().length() > 1) { + return false; + } + + event.preventDefault(); + + Touch touch = event.getChangedTouches().get(0); + double diffX = Math.abs(touch.getClientX() + - rowEventTouchStartingPoint.getX()); + double diffY = Math.abs(touch.getClientY() + - rowEventTouchStartingPoint.getY()); + + // Cancel long tap if finger strays too far from + // starting point + if (diffX > GridConstants.LONG_TAP_THRESHOLD + || diffY > GridConstants.LONG_TAP_THRESHOLD) { + lazySorter.cancel(); + } + + } else if (BrowserEvents.TOUCHEND.equals(event.getType())) { + if (event.getTouches().length() > 0) { + return false; + } + + if (lazySorter.isRunning()) { + // Not a long tap yet, perform single sort + lazySorter.cancel(); + lazySorter.setMultisort(false); + lazySorter.run(); + } + + } else if (BrowserEvents.TOUCHCANCEL.equals(event.getType())) { + if (event.getChangedTouches().length() > 1) { + return false; + } + + lazySorter.cancel(); + + } else if (BrowserEvents.CLICK.equals(event.getType())) { + lazySorter.setCellReference(cell); + lazySorter.setMultisort(event.getShiftKey()); + lazySorter.run(); + + // Active cell handling is also monitoring the click + // event so we allow event to propagate for it + return false; + } + + return true; + } + + @Override + public com.google.gwt.user.client.Element getSubPartElement(String subPart) { + // Parse SubPart string to type and indices + String[] splitArgs = subPart.split("\\["); + + String type = splitArgs[0]; + int[] indices = new int[splitArgs.length - 1]; + for (int i = 0; i < indices.length; ++i) { + String tmp = splitArgs[i + 1]; + indices[i] = Integer.parseInt(tmp.substring(0, tmp.length() - 1)); + } + + // Get correct RowContainer for type from Escalator + RowContainer container = null; + if (type.equalsIgnoreCase("header")) { + container = escalator.getHeader(); + } else if (type.equalsIgnoreCase("cell")) { + // If wanted row is not visible, we need to scroll there. + Range visibleRowRange = escalator.getVisibleRowRange(); + if (indices.length > 0 && !visibleRowRange.contains(indices[0])) { + try { + scrollToRow(indices[0]); + } catch (IllegalArgumentException e) { + getLogger().log(Level.SEVERE, e.getMessage()); + } + // Scrolling causes a lazy loading event. No element can + // currently be retrieved. + return null; + } + container = escalator.getBody(); + } else if (type.equalsIgnoreCase("footer")) { + container = escalator.getFooter(); + } + + if (null != container) { + if (indices.length == 0) { + // No indexing. Just return the wanted container element + return DOM.asOld(container.getElement()); + } else { + try { + return DOM.asOld(getSubPart(container, indices)); + } catch (Exception e) { + getLogger().log(Level.SEVERE, e.getMessage()); + } + } + } + return null; + } + + private Element getSubPart(RowContainer container, int[] indices) { + // Scroll wanted column to view if able + if (indices.length > 1 + && escalator.getColumnConfiguration().getFrozenColumnCount() <= indices[1]) { + escalator.scrollToColumn(indices[1], ScrollDestination.ANY, 0); + } + + Element targetElement = container.getRowElement(indices[0]); + for (int i = 1; i < indices.length && targetElement != null; ++i) { + targetElement = (Element) targetElement.getChild(indices[i]); + } + return targetElement; + } + + @Override + public String getSubPartName(com.google.gwt.user.client.Element subElement) { + // Containers and matching SubPart types + List<RowContainer> containers = Arrays.asList(escalator.getHeader(), + escalator.getBody(), escalator.getFooter()); + List<String> containerType = Arrays.asList("header", "cell", "footer"); + + for (int i = 0; i < containers.size(); ++i) { + RowContainer container = containers.get(i); + boolean containerRow = (subElement.getTagName().equalsIgnoreCase( + "tr") && subElement.getParentElement() == container + .getElement()); + if (containerRow) { + // Wanted SubPart is row that is a child of containers root + // To get indices, we use a cell that is a child of this row + subElement = DOM.asOld(subElement.getFirstChildElement()); + } + + Cell cell = container.getCell(subElement); + if (cell != null) { + // Skip the column index if subElement was a child of root + return containerType.get(i) + "[" + cell.getRow() + + (containerRow ? "]" : "][" + cell.getColumn() + "]"); + } + } + return null; + } + + private void setSelectColumnRenderer( + final Renderer<Boolean> selectColumnRenderer) { + if (this.selectColumnRenderer == selectColumnRenderer) { + return; + } + + if (this.selectColumnRenderer != null) { + removeColumnSkipSelectionColumnCheck(selectionColumn); + --activeCellHandler.activeColumn; + } + + this.selectColumnRenderer = selectColumnRenderer; + + if (selectColumnRenderer != null) { + ++activeCellHandler.activeColumn; + selectionColumn = new SelectionColumn(selectColumnRenderer); + + // FIXME: this needs to be done elsewhere, requires design... + selectionColumn.setWidth(25); + addColumnSkipSelectionColumnCheck(selectionColumn, 0); + selectionColumn.initDone(); + } else { + selectionColumn = null; + refreshBody(); + } + } + + /** + * Accesses the package private method Widget#setParent() + * + * @param widget + * The widget to access + * @param parent + * The parent to set + */ + private static native final void setParent(Widget widget, Widget parent) + /*-{ + widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent); + }-*/; + + /** + * Sets the current selection model. + * <p> + * This function will call {@link SelectionModel#setGrid(Grid)}. + * + * @param selectionModel + * a selection model implementation. + * @throws IllegalArgumentException + * if selection model argument is null + */ + public void setSelectionModel(SelectionModel<T> selectionModel) { + + if (selectionModel == null) { + throw new IllegalArgumentException("Selection model can't be null"); + } + + if (selectColumnRenderer != null + && selectColumnRenderer instanceof ComplexRenderer) { + ((ComplexRenderer<?>) selectColumnRenderer).destroy(); + } + + this.selectionModel = selectionModel; + selectionModel.setGrid(this); + setSelectColumnRenderer(this.selectionModel + .getSelectionColumnRenderer()); + } + + /** + * Gets a reference to the current selection model. + * + * @return the currently used SelectionModel instance. + */ + public SelectionModel<T> getSelectionModel() { + return selectionModel; + } + + /** + * Sets current selection mode. + * <p> + * This is a shorthand method for {@link Grid#setSelectionModel}. + * + * @param mode + * a selection mode value + * @see {@link SelectionMode}. + */ + public void setSelectionMode(SelectionMode mode) { + SelectionModel<T> model = mode.createModel(); + setSelectionModel(model); + } + + /** + * Test if a row is selected. + * + * @param row + * a row object + * @return true, if the current selection model considers the provided row + * object selected. + */ + public boolean isSelected(T row) { + return selectionModel.isSelected(row); + } + + /** + * Select a row using the current selection model. + * <p> + * Only selection models implementing {@link SelectionModel.Single} and + * {@link SelectionModel.Multi} are supported; for anything else, an + * exception will be thrown. + * + * @param row + * a row object + * @return <code>true</code> iff the current selection changed + * @throws IllegalStateException + * if the current selection model is not an instance of + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + @SuppressWarnings("unchecked") + public boolean select(T row) { + if (selectionModel instanceof SelectionModel.Single<?>) { + return ((SelectionModel.Single<T>) selectionModel).select(row); + } else if (selectionModel instanceof SelectionModel.Multi<?>) { + return ((SelectionModel.Multi<T>) selectionModel).select(row); + } else { + throw new IllegalStateException("Unsupported selection model"); + } + } + + /** + * Deselect a row using the current selection model. + * <p> + * Only selection models implementing {@link SelectionModel.Single} and + * {@link SelectionModel.Multi} are supported; for anything else, an + * exception will be thrown. + * + * @param row + * a row object + * @return <code>true</code> iff the current selection changed + * @throws IllegalStateException + * if the current selection model is not an instance of + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + @SuppressWarnings("unchecked") + public boolean deselect(T row) { + if (selectionModel instanceof SelectionModel.Single<?>) { + return ((SelectionModel.Single<T>) selectionModel).deselect(row); + } else if (selectionModel instanceof SelectionModel.Multi<?>) { + return ((SelectionModel.Multi<T>) selectionModel).deselect(row); + } else { + throw new IllegalStateException("Unsupported selection model"); + } + } + + /** + * Gets last selected row from the current SelectionModel. + * <p> + * Only selection models implementing {@link SelectionModel.Single} are + * valid for this method; for anything else, use the + * {@link Grid#getSelectedRows()} method. + * + * @return a selected row reference, or null, if no row is selected + * @throws IllegalStateException + * if the current selection model is not an instance of + * {@link SelectionModel.Single} + */ + public T getSelectedRow() { + if (selectionModel instanceof SelectionModel.Single<?>) { + return ((SelectionModel.Single<T>) selectionModel).getSelectedRow(); + } else { + throw new IllegalStateException( + "Unsupported selection model; can not get single selected row"); + } + } + + /** + * Gets currently selected rows from the current selection model. + * + * @return a non-null collection containing all currently selected rows. + */ + public Collection<T> getSelectedRows() { + return selectionModel.getSelectedRows(); + } + + @Override + public HandlerRegistration addSelectionChangeHandler( + final SelectionChangeHandler<T> handler) { + return addHandler(handler, SelectionChangeEvent.getType()); + } + + /** + * Sets the current sort order using the fluid Sort API. Read the + * documentation for {@link Sort} for more information. + * + * @param s + * a sort instance + */ + public void sort(Sort s) { + setSortOrder(s.build()); + } + + /** + * Sorts the Grid data in ascending order along one column. + * + * @param column + * a grid column reference + */ + public <C> void sort(GridColumn<C, T> column) { + sort(column, SortDirection.ASCENDING); + } + + /** + * Sorts the Grid data along one column. + * + * @param column + * a grid column reference + * @param direction + * a sort direction value + */ + public <C> void sort(GridColumn<C, T> column, SortDirection direction) { + sort(Sort.by(column, direction)); + } + + /** + * Sets the sort order to use. Setting this causes the Grid to re-sort + * itself. + * + * @param order + * a sort order list. If set to null, the sort order is cleared. + */ + public void setSortOrder(List<SortOrder> order) { + setSortOrder(order, SortEventOriginator.API); + } + + private void setSortOrder(List<SortOrder> order, + SortEventOriginator originator) { + sortOrder.clear(); + if (order != null) { + sortOrder.addAll(order); + } + sort(originator); + } + + /** + * Get a copy of the current sort order array. + * + * @return a copy of the current sort order array + */ + public List<SortOrder> getSortOrder() { + return Collections.unmodifiableList(sortOrder); + } + + /** + * Finds the sorting order for this column + */ + private SortOrder getSortOrder(GridColumn<?, ?> column) { + for (SortOrder order : getSortOrder()) { + if (order.getColumn() == column) { + return order; + } + } + return null; + } + + /** + * Register a GWT event handler for a sorting event. This handler gets + * called whenever this Grid needs its data source to provide data sorted in + * a specific order. + * + * @param handler + * a sort event handler + * @return the registration for the event + */ + public HandlerRegistration addSortHandler(SortEventHandler<T> handler) { + return addHandler(handler, SortEvent.getType()); + } + + /** + * Register a GWT event handler for a data available event. This handler + * gets called whenever the {@link DataSource} for this Grid has new data + * available. + * <p> + * This handle will be fired with the current available data after + * registration is done. + * + * @param handler + * a data available event handler + * @return the registartion for the event + */ + public HandlerRegistration addDataAvailableHandler( + final DataAvailableHandler handler) { + // Deferred call to handler with current row range + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @Override + public void execute() { + handler.onDataAvailable(new DataAvailableEvent( + currentDataAvailable)); + } + }); + return addHandler(handler, DataAvailableEvent.TYPE); + } + + /** + * Register a KeyDown handler to this Grid. If the handler is a + * HeaderKeyDownHandler, it will be fired only when a header cell is active. + * The same goes for body and footer with their respective handlers. + * + * @param handler + * the key handler to register + * @return the registration for the event + */ + public <HANDLER extends GridKeyDownHandler<T>> HandlerRegistration addKeyDownHandler( + HANDLER handler) { + if (handler instanceof BodyKeyDownHandler + || handler instanceof HeaderKeyDownHandler + || handler instanceof FooterKeyDownHandler) { + return addHandler(handler, keyDown.getAssociatedType()); + } + throw new IllegalArgumentException( + "Handler not a valid extension of GridKeyDownHandler"); + } + + /** + * Register a KeyUp handler to this Grid. If the handler is a + * HeaderKeyUpHandler, it will be fired only when a header cell is active. + * The same goes for body and footer with their respective handlers. + * + * @param handler + * the key handler to register + * @return the registration for the event + */ + public <HANDLER extends GridKeyUpHandler<T>> HandlerRegistration addKeyUpHandler( + HANDLER handler) { + if (handler instanceof BodyKeyUpHandler + || handler instanceof HeaderKeyUpHandler + || handler instanceof FooterKeyUpHandler) { + return addHandler(handler, keyUp.getAssociatedType()); + } + throw new IllegalArgumentException( + "Handler not a valid extension of GridKeyUpHandler"); + } + + /** + * Register a KeyPress handler to this Grid. If the handler is a + * HeaderKeyPressHandler, it will be fired only when a header cell is + * active. The same goes for body and footer with their respective handlers. + * + * @param handler + * the key handler to register + * @return the registration for the event + */ + public <HANDLER extends GridKeyPressHandler<T>> HandlerRegistration addKeyPressHandler( + HANDLER handler) { + if (handler instanceof BodyKeyPressHandler + || handler instanceof HeaderKeyPressHandler + || handler instanceof FooterKeyPressHandler) { + return addHandler(handler, keyPress.getAssociatedType()); + } + throw new IllegalArgumentException( + "Handler not a valid extension of GridKeyPressHandler"); + } + + /** + * Apply sorting to data source. + */ + private void sort(SortEventOriginator originator) { + refreshHeader(); + fireEvent(new SortEvent<T>(this, + Collections.unmodifiableList(sortOrder), originator)); + } + + private int getLastVisibleRowIndex() { + int lastRowIndex = escalator.getVisibleRowRange().getEnd(); + int footerTop = escalator.getFooter().getElement().getAbsoluteTop(); + Element lastRow; + + do { + lastRow = escalator.getBody().getRowElement(--lastRowIndex); + } while (lastRow.getAbsoluteBottom() > footerTop); + + return lastRowIndex; + } + + private int getFirstVisibleRowIndex() { + int firstRowIndex = escalator.getVisibleRowRange().getStart(); + int headerBottom = escalator.getHeader().getElement() + .getAbsoluteBottom(); + Element firstRow = escalator.getBody().getRowElement(firstRowIndex); + + while (firstRow.getAbsoluteTop() < headerBottom) { + firstRow = escalator.getBody().getRowElement(++firstRowIndex); + } + + return firstRowIndex; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridColumn.java b/client/src/com/vaadin/client/ui/grid/GridColumn.java new file mode 100644 index 0000000000..69be2d5532 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridColumn.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.ui.grid; + +/** + * Represents a column in the {@link Grid}. + * + * @param <C> + * The column type + * + * @param <T> + * The row type + * + * @since + * @author Vaadin Ltd + */ +public abstract class GridColumn<C, T> extends Grid.AbstractGridColumn<C, T> { + + /* + * This class is a convenience class so you do not have to reference + * Grid.AbstractGridColumn in your production code. The real implementation + * should be in the abstract class. + */ + + /** + * Constructs a new column with a custom renderer. + * + * @param renderer + * The renderer to use for rendering the cells + */ + public GridColumn(Renderer<? super C> renderer) { + super(renderer); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java new file mode 100644 index 0000000000..73440232b1 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -0,0 +1,610 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.client.data.RpcDataSourceConnector.RpcDataSource; +import com.vaadin.client.ui.AbstractHasComponentsConnector; +import com.vaadin.client.ui.grid.GridHeader.HeaderRow; +import com.vaadin.client.ui.grid.GridStaticSection.StaticCell; +import com.vaadin.client.ui.grid.GridStaticSection.StaticRow; +import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector; +import com.vaadin.client.ui.grid.selection.AbstractRowHandleSelectionModel; +import com.vaadin.client.ui.grid.selection.SelectionChangeEvent; +import com.vaadin.client.ui.grid.selection.SelectionChangeHandler; +import com.vaadin.client.ui.grid.selection.SelectionModel; +import com.vaadin.client.ui.grid.selection.SelectionModelMulti; +import com.vaadin.client.ui.grid.selection.SelectionModelNone; +import com.vaadin.client.ui.grid.selection.SelectionModelSingle; +import com.vaadin.client.ui.grid.sort.SortEvent; +import com.vaadin.client.ui.grid.sort.SortEventHandler; +import com.vaadin.client.ui.grid.sort.SortOrder; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.GridClientRpc; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode; +import com.vaadin.shared.ui.grid.GridStaticSectionState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Connects the client side {@link Grid} widget with the server side + * {@link com.vaadin.ui.components.grid.Grid} component. + * <p> + * The Grid is typed to JSONObject. The structure of the JSONObject is described + * at {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List) + * DataProviderRpc.setRowData(int, List)}. + * + * @since + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.Grid.class) +public class GridConnector extends AbstractHasComponentsConnector { + + /** + * Custom implementation of the custom grid column using a JSONObject to + * represent the cell value and String as a column type. + */ + private class CustomGridColumn extends GridColumn<Object, JSONObject> { + + private final String id; + + private AbstractRendererConnector<Object> rendererConnector; + + public CustomGridColumn(String id, + AbstractRendererConnector<Object> rendererConnector) { + super(rendererConnector.getRenderer()); + this.rendererConnector = rendererConnector; + this.id = id; + } + + @Override + public Object getValue(final JSONObject obj) { + final JSONValue rowData = obj.get(GridState.JSONKEY_DATA); + final JSONArray rowDataArray = rowData.isArray(); + assert rowDataArray != null : "Was unable to parse JSON into an array: " + + rowData; + + final int columnIndex = resolveCurrentIndexFromState(); + final JSONValue columnValue = rowDataArray.get(columnIndex); + return rendererConnector.decode(columnValue); + } + + /* + * Only used to check that the renderer connector will not change during + * the column lifetime. + * + * TODO remove once support for changing renderers is implemented + */ + private AbstractRendererConnector<Object> getRendererConnector() { + return rendererConnector; + } + + private int resolveCurrentIndexFromState() { + List<GridColumnState> columns = getState().columns; + int numColumns = columns.size(); + for (int index = 0; index < numColumns; index++) { + if (columns.get(index).id.equals(id)) { + return index; + } + } + return -1; + } + } + + /** + * Maps a generated column id to a grid column instance + */ + private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>(); + + private AbstractRowHandleSelectionModel<JSONObject> selectionModel = createSelectionModel(SharedSelectionMode.NONE); + private Set<String> selectedKeys = new LinkedHashSet<String>(); + + /** + * updateFromState is set to true when {@link #updateSelectionFromState()} + * makes changes to selection. This flag tells the + * {@code internalSelectionChangeHandler} to not send same data straight + * back to server. Said listener sets it back to false when handling that + * event. + */ + private boolean updatedFromState = false; + + private RpcDataSource dataSource; + + private SelectionChangeHandler<JSONObject> internalSelectionChangeHandler = new SelectionChangeHandler<JSONObject>() { + @Override + public void onSelectionChange(SelectionChangeEvent<JSONObject> event) { + if (event.isBatchedSelection()) { + return; + } + if (!updatedFromState) { + for (JSONObject row : event.getRemoved()) { + selectedKeys.remove(dataSource.getRowKey(row)); + } + + for (JSONObject row : event.getAdded()) { + selectedKeys.add((String) dataSource.getRowKey(row)); + } + + getRpcProxy(GridServerRpc.class).selectionChange( + new ArrayList<String>(selectedKeys)); + } else { + updatedFromState = false; + } + } + }; + + @Override + @SuppressWarnings("unchecked") + public Grid<JSONObject> getWidget() { + return (Grid<JSONObject>) super.getWidget(); + } + + @Override + public GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected void init() { + super.init(); + + registerRpc(GridClientRpc.class, new GridClientRpc() { + @Override + public void scrollToStart() { + getWidget().scrollToStart(); + } + + @Override + public void scrollToEnd() { + getWidget().scrollToEnd(); + } + + @Override + public void scrollToRow(int row, ScrollDestination destination) { + getWidget().scrollToRow(row, destination); + } + }); + + getWidget().setSelectionModel(selectionModel); + + getWidget().addSelectionChangeHandler(internalSelectionChangeHandler); + + getWidget().addSortHandler(new SortEventHandler<JSONObject>() { + @Override + public void sort(SortEvent<JSONObject> event) { + List<SortOrder> order = event.getOrder(); + String[] columnIds = new String[order.size()]; + SortDirection[] directions = new SortDirection[order.size()]; + for (int i = 0; i < order.size(); i++) { + SortOrder sortOrder = order.get(i); + CustomGridColumn column = (CustomGridColumn) sortOrder + .getColumn(); + columnIds[i] = column.id; + + directions[i] = sortOrder.getDirection(); + } + + if (!Arrays.equals(columnIds, getState().sortColumns) + || !Arrays.equals(directions, getState().sortDirs)) { + // Report back to server if changed + getRpcProxy(GridServerRpc.class).sort(columnIds, + directions, event.getOriginator()); + } + } + }); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + // Column updates + if (stateChangeEvent.hasPropertyChanged("columns")) { + + int totalColumns = getState().columns.size(); + + // Remove old columns + purgeRemovedColumns(); + + int currentColumns = getWidget().getColumnCount(); + if (getWidget().getSelectionModel().getSelectionColumnRenderer() != null) { + currentColumns--; + } + + // Add new columns + for (int columnIndex = currentColumns; columnIndex < totalColumns; columnIndex++) { + addColumnFromStateChangeEvent(columnIndex); + } + + // Update old columns + for (int columnIndex = 0; columnIndex < currentColumns; columnIndex++) { + // FIXME Currently updating all column header / footers when a + // change in made in one column. When the framework supports + // quering a specific item in a list then it should do so here. + updateColumnFromStateChangeEvent(columnIndex); + } + } + + if (stateChangeEvent.hasPropertyChanged("header")) { + updateSectionFromState(getWidget().getHeader(), getState().header); + } + + if (stateChangeEvent.hasPropertyChanged("footer")) { + updateSectionFromState(getWidget().getFooter(), getState().footer); + } + + if (stateChangeEvent.hasPropertyChanged("lastFrozenColumnId")) { + String frozenColId = getState().lastFrozenColumnId; + if (frozenColId != null) { + CustomGridColumn column = columnIdToColumn.get(frozenColId); + assert column != null : "Column to be frozen could not be found (id:" + + frozenColId + ")"; + getWidget().setLastFrozenColumn(column); + } else { + getWidget().setLastFrozenColumn(null); + } + } + } + + private void updateSectionFromState(GridStaticSection<?> section, + GridStaticSectionState state) { + + while (section.getRowCount() != 0) { + section.removeRow(0); + } + + for (RowState rowState : state.rows) { + StaticRow<?> row = section.appendRow(); + + int selectionOffset = 1; + if (getWidget().getSelectionModel() instanceof SelectionModel.None) { + selectionOffset = 0; + } + + assert rowState.cells.size() == getWidget().getColumnCount() - selectionOffset; + + int i = 0 + selectionOffset; + for (CellState cellState : rowState.cells) { + StaticCell cell = row.getCell(i++); + switch (cellState.type) { + case TEXT: + cell.setText(cellState.text); + break; + case HTML: + cell.setHtml(cellState.text); + break; + case WIDGET: + ComponentConnector connector = (ComponentConnector) cellState.connector; + cell.setWidget(connector.getWidget()); + break; + default: + throw new IllegalStateException("unexpected cell type: " + + cellState.type); + } + } + + for (List<Integer> group : rowState.cellGroups) { + GridColumn<?, ?>[] columns = new GridColumn<?, ?>[group.size()]; + i = 0; + for (Integer colIndex : group) { + columns[i++] = getWidget().getColumn(selectionOffset + colIndex); + } + row.join(columns); + } + + if (section instanceof GridHeader && rowState.defaultRow) { + ((GridHeader) section).setDefaultRow((HeaderRow) row); + } + } + + section.setVisible(state.visible); + + section.requestSectionRefresh(); + } + + /** + * Updates a column from a state change event. + * + * @param columnIndex + * The index of the column to update + */ + private void updateColumnFromStateChangeEvent(final int columnIndex) { + /* + * We use the widget column index here instead of the given column + * index. SharedState contains information only about the explicitly + * defined columns, while the widget counts the selection column as an + * explicit one. + */ + GridColumn<?, JSONObject> column = getWidget().getColumn( + getWidgetColumnIndex(columnIndex)); + + GridColumnState columnState = getState().columns.get(columnIndex); + updateColumnFromState(column, columnState); + + assert column instanceof CustomGridColumn : "column at index " + + columnIndex + " is not a " + + CustomGridColumn.class.getSimpleName() + ", but a " + + column.getClass().getSimpleName(); + + if (columnState.rendererConnector != ((CustomGridColumn) column) + .getRendererConnector()) { + throw new UnsupportedOperationException( + "Changing column renderer after initialization is currently unsupported"); + } + } + + /** + * Adds a new column to the grid widget from a state change event + * + * @param columnIndex + * The index of the column, according to how it + */ + private void addColumnFromStateChangeEvent(int columnIndex) { + GridColumnState state = getState().columns.get(columnIndex); + @SuppressWarnings("unchecked") + CustomGridColumn column = new CustomGridColumn(state.id, + ((AbstractRendererConnector<Object>) state.rendererConnector)); + columnIdToColumn.put(state.id, column); + + /* + * Adds a column to grid, and registers Grid with the column. + * + * We use the widget column index here instead of the given column + * index. SharedState contains information only about the explicitly + * defined columns, while the widget counts the selection column as an + * explicit one. + */ + getWidget().addColumn(column, getWidgetColumnIndex(columnIndex)); + + /* + * Have to update state _after_ the column has been added to the grid as + * then, and only then, the column will call the grid which in turn will + * call the escalator's refreshRow methods on header/footer/body and + * visually refresh the row. If this is done in the reverse order the + * first column state update will be lost as no grid instance is + * present. + */ + updateColumnFromState(column, state); + } + + /** + * If we have a selection column renderer, we need to offset the index by + * one when referring to the column index in the widget. + */ + private int getWidgetColumnIndex(final int columnIndex) { + Renderer<Boolean> selectionColumnRenderer = getWidget() + .getSelectionModel().getSelectionColumnRenderer(); + int widgetColumnIndex = columnIndex; + if (selectionColumnRenderer != null) { + widgetColumnIndex++; + } + return widgetColumnIndex; + } + + /** + * Updates the column values from a state + * + * @param column + * The column to update + * @param state + * The state to get the data from + */ + private static void updateColumnFromState(GridColumn<?, JSONObject> column, + GridColumnState state) { + column.setVisible(state.visible); + column.setWidth(state.width); + column.setSortable(state.sortable); + } + + /** + * Removes any orphan columns that has been removed from the state from the + * grid + */ + private void purgeRemovedColumns() { + + // Get columns still registered in the state + Set<String> columnsInState = new HashSet<String>(); + for (GridColumnState columnState : getState().columns) { + columnsInState.add(columnState.id); + } + + // Remove column no longer in state + Iterator<String> columnIdIterator = columnIdToColumn.keySet() + .iterator(); + while (columnIdIterator.hasNext()) { + String id = columnIdIterator.next(); + if (!columnsInState.contains(id)) { + CustomGridColumn column = columnIdToColumn.get(id); + columnIdIterator.remove(); + getWidget().removeColumn(column); + } + } + } + + public void setDataSource(RpcDataSource dataSource) { + this.dataSource = dataSource; + getWidget().setDataSource(this.dataSource); + } + + @OnStateChange("selectionMode") + private void onSelectionModeChange() { + SharedSelectionMode mode = getState().selectionMode; + if (mode == null) { + getLogger().fine("ignored mode change"); + return; + } + + AbstractRowHandleSelectionModel<JSONObject> model = createSelectionModel(mode); + if (!model.getClass().equals(selectionModel.getClass())) { + selectionModel = model; + getWidget().setSelectionModel(model); + selectedKeys.clear(); + } + } + + @OnStateChange("selectedKeys") + private void updateSelectionFromState() { + boolean changed = false; + + List<String> stateKeys = getState().selectedKeys; + + // find new deselections + for (String key : selectedKeys) { + if (!stateKeys.contains(key)) { + changed = true; + deselectByHandle(dataSource.getHandleByKey(key)); + } + } + + // find new selections + for (String key : stateKeys) { + if (!selectedKeys.contains(key)) { + changed = true; + selectByHandle(dataSource.getHandleByKey(key)); + } + } + + /* + * A defensive copy in case the collection in the state is mutated + * instead of re-assigned. + */ + selectedKeys = new LinkedHashSet<String>(stateKeys); + + /* + * We need to fire this event so that Grid is able to re-render the + * selection changes (if applicable). + */ + if (changed) { + // At least for now there's no way to send the selected and/or + // deselected row data. Some data is only stored as keys + updatedFromState = true; + getWidget().fireEvent( + new SelectionChangeEvent<JSONObject>(getWidget(), + (List<JSONObject>) null, null, false)); + } + } + + @OnStateChange({ "sortColumns", "sortDirs" }) + private void onSortStateChange() { + List<SortOrder> sortOrder = new ArrayList<SortOrder>(); + + String[] sortColumns = getState().sortColumns; + SortDirection[] sortDirs = getState().sortDirs; + + for (int i = 0; i < sortColumns.length; i++) { + sortOrder.add(new SortOrder(columnIdToColumn.get(sortColumns[i]), + sortDirs[i])); + } + + getWidget().setSortOrder(sortOrder); + } + + private Logger getLogger() { + return Logger.getLogger(getClass().getName()); + } + + @SuppressWarnings("static-method") + private AbstractRowHandleSelectionModel<JSONObject> createSelectionModel( + SharedSelectionMode mode) { + switch (mode) { + case SINGLE: + return new SelectionModelSingle<JSONObject>(); + case MULTI: + return new SelectionModelMulti<JSONObject>(); + case NONE: + return new SelectionModelNone<JSONObject>(); + default: + throw new IllegalStateException("unexpected mode value: " + mode); + } + } + + /** + * A workaround method for accessing the protected method + * {@code AbstractRowHandleSelectionModel.selectByHandle} + */ + private native void selectByHandle(RowHandle<JSONObject> handle) + /*-{ + var model = this.@com.vaadin.client.ui.grid.GridConnector::selectionModel; + model.@com.vaadin.client.ui.grid.selection.AbstractRowHandleSelectionModel::selectByHandle(*)(handle); + }-*/; + + /** + * A workaround method for accessing the protected method + * {@code AbstractRowHandleSelectionModel.deselectByHandle} + */ + private native void deselectByHandle(RowHandle<JSONObject> handle) + /*-{ + var model = this.@com.vaadin.client.ui.grid.GridConnector::selectionModel; + model.@com.vaadin.client.ui.grid.selection.AbstractRowHandleSelectionModel::deselectByHandle(*)(handle); + }-*/; + + /** + * Gets the row key for a row by index. + * + * @param index + * the index of the row for which to get the key + * @return the key for the row at {@code index} + */ + public String getRowKey(int index) { + final JSONObject row = dataSource.getRow(index); + final Object key = dataSource.getRowKey(row); + assert key instanceof String : "Internal key was not a String but a " + + key.getClass().getSimpleName() + " (" + key + ")"; + return (String) key; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.client.HasComponentsConnector#updateCaption(com.vaadin.client + * .ComponentConnector) + */ + @Override + public void updateCaption(ComponentConnector connector) { + // TODO Auto-generated method stub + + } + + @Override + public void onConnectorHierarchyChange( + ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) { + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridFooter.java b/client/src/com/vaadin/client/ui/grid/GridFooter.java new file mode 100644 index 0000000000..e798139b9a --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridFooter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import com.google.gwt.core.client.Scheduler; + +/** + * Represents the footer section of a Grid. The footer is always empty. + * + * @since + * @author Vaadin Ltd + */ +public class GridFooter extends GridStaticSection<GridFooter.FooterRow> { + + /** + * A single row in a grid Footer section. + * + */ + public class FooterRow extends GridStaticSection.StaticRow<FooterCell> { + + @Override + protected FooterCell createCell() { + return new FooterCell(); + } + } + + /** + * A single cell in a grid Footer row. Has a textual caption. + * + */ + public class FooterCell extends GridStaticSection.StaticCell { + } + + private boolean markAsDirty = false; + + @Override + protected FooterRow createRow() { + return new FooterRow(); + } + + @Override + protected void requestSectionRefresh() { + markAsDirty = true; + + /* + * Defer the refresh so if we multiple times call refreshSection() (for + * example when updating cell values) we only get one actual refresh in + * the end. + */ + Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { + + @Override + public void execute() { + if (markAsDirty) { + markAsDirty = false; + getGrid().refreshFooter(); + } + } + }); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridHeader.java b/client/src/com/vaadin/client/ui/grid/GridHeader.java new file mode 100644 index 0000000000..2867ae4d1c --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridHeader.java @@ -0,0 +1,150 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import java.util.Arrays; +import java.util.Collection; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.BrowserEvents; + +/** + * Represents the header section of a Grid. A header consists of a single header + * row containing a header cell for each column. Each cell has a simple textual + * caption. + * + * @since + * @author Vaadin Ltd + */ +public class GridHeader extends GridStaticSection<GridHeader.HeaderRow> { + + /** + * A single row in a grid header section. + * + */ + public class HeaderRow extends GridStaticSection.StaticRow<HeaderCell> { + + private boolean isDefault = false; + + protected void setDefault(boolean isDefault) { + this.isDefault = isDefault; + } + + public boolean isDefault() { + return isDefault; + } + + @Override + protected HeaderCell createCell() { + return new HeaderCell(); + } + } + + /** + * A single cell in a grid header row. Has a textual caption. + * + */ + public class HeaderCell extends GridStaticSection.StaticCell { + } + + private HeaderRow defaultRow; + + private boolean markAsDirty = false; + + @Override + public void removeRow(int index) { + HeaderRow removedRow = getRow(index); + super.removeRow(index); + if (removedRow == defaultRow) { + setDefaultRow(null); + } + } + + /** + * Sets the default row of this header. The default row is a special header + * row providing a user interface for sorting columns. + * + * @param row + * the new default row, or null for no default row + * + * @throws IllegalArgumentException + * this header does not contain the row + */ + public void setDefaultRow(HeaderRow row) { + if (row == defaultRow) { + return; + } + if (row != null && !getRows().contains(row)) { + throw new IllegalArgumentException( + "Cannot set a default row that does not exist in the container"); + } + if (defaultRow != null) { + defaultRow.setDefault(false); + } + if (row != null) { + row.setDefault(true); + } + defaultRow = row; + requestSectionRefresh(); + } + + /** + * Returns the current default row of this header. The default row is a + * special header row providing a user interface for sorting columns. + * + * @return the default row or null if no default row set + */ + public HeaderRow getDefaultRow() { + return defaultRow; + } + + @Override + protected HeaderRow createRow() { + return new HeaderRow(); + } + + @Override + protected void requestSectionRefresh() { + markAsDirty = true; + + /* + * Defer the refresh so if we multiple times call refreshSection() (for + * example when updating cell values) we only get one actual refresh in + * the end. + */ + Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { + + @Override + public void execute() { + if (markAsDirty) { + markAsDirty = false; + getGrid().refreshHeader(); + } + } + }); + } + + /** + * Returns the events consumed by the header + * + * @return a collection of BrowserEvents + */ + public Collection<String> getConsumedEvents() { + return Arrays.asList(BrowserEvents.TOUCHSTART, BrowserEvents.TOUCHMOVE, + BrowserEvents.TOUCHEND, BrowserEvents.TOUCHCANCEL, + BrowserEvents.CLICK); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridStaticSection.java b/client/src/com/vaadin/client/ui/grid/GridStaticSection.java new file mode 100644 index 0000000000..8c9ada46d0 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridStaticSection.java @@ -0,0 +1,532 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.shared.ui.grid.GridStaticCellType; + +/** + * Abstract base class for Grid header and footer sections. + * + * @since + * @author Vaadin Ltd + * @param <ROWTYPE> + * the type of the rows in the section + */ +abstract class GridStaticSection<ROWTYPE extends GridStaticSection.StaticRow<?>> { + + /** + * A header or footer cell. Has a simple textual caption. + * + */ + static class StaticCell { + + private Object content = null; + + private int colspan = 1; + + private GridStaticSection<?> section; + + private GridStaticCellType type = GridStaticCellType.TEXT; + + /** + * Sets the text displayed in this cell. + * + * @param text + * a plain text caption + */ + public void setText(String text) { + this.content = text; + this.type = GridStaticCellType.TEXT; + section.requestSectionRefresh(); + } + + /** + * Returns the text displayed in this cell. + * + * @return the plain text caption + */ + public String getText() { + if (type != GridStaticCellType.TEXT) { + throw new IllegalStateException( + "Cannot fetch Text from a cell with type " + type); + } + return (String) content; + } + + protected GridStaticSection<?> getSection() { + assert section != null; + return section; + } + + protected void setSection(GridStaticSection<?> section) { + this.section = section; + } + + /** + * Returns the amount of columns the cell spans. By default is 1. + * + * @return The amount of columns the cell spans. + */ + public int getColspan() { + return colspan; + } + + /** + * Sets the amount of columns the cell spans. Must be more or equal to + * 1. By default is 1. + * + * @param colspan + * the colspan to set + */ + public void setColspan(int colspan) { + if (colspan < 1) { + throw new IllegalArgumentException( + "Colspan cannot be less than 1"); + } + + this.colspan = colspan; + section.requestSectionRefresh(); + } + + /** + * Returns the html inside the cell. + * + * @throws IllegalStateException + * if trying to retrive HTML from a cell with a type other + * than {@link Type#HTML}. + * @return the html content of the cell. + */ + public String getHtml() { + if (type != GridStaticCellType.HTML) { + throw new IllegalStateException( + "Cannot fetch HTML from a cell with type " + type); + } + return (String) content; + } + + /** + * Sets the content of the cell to the provided html. All previous + * content is discarded and the cell type is set to {@link Type#HTML}. + * + * @param html + * The html content of the cell + */ + public void setHtml(String html) { + this.content = html; + this.type = GridStaticCellType.HTML; + section.requestSectionRefresh(); + } + + /** + * Returns the widget in the cell. + * + * @throws IllegalStateException + * if the cell is not {@link Type#WIDGET} + * + * @return the widget in the cell + */ + public Widget getWidget() { + if (type != GridStaticCellType.WIDGET) { + throw new IllegalStateException( + "Cannot fetch Widget from a cell with type " + type); + } + return (Widget) content; + } + + /** + * Set widget as the content of the cell. The type of the cell becomes + * {@link Type#WIDGET}. All previous content is discarded. + * + * @param widget + * The widget to add to the cell. Should not be previously + * attached anywhere (widget.getParent == null). + */ + public void setWidget(Widget widget) { + this.content = widget; + this.type = GridStaticCellType.WIDGET; + section.requestSectionRefresh(); + } + + /** + * Returns the type of the cell. + * + * @return the type of content the cell contains. + */ + public GridStaticCellType getType() { + return type; + } + } + + /** + * Abstract base class for Grid header and footer rows. + * + * @param <CELLTYPE> + * the type of the cells in the row + */ + abstract static class StaticRow<CELLTYPE extends StaticCell> { + + private List<CELLTYPE> cells = new ArrayList<CELLTYPE>(); + + private GridStaticSection<?> section; + + private Collection<List<CELLTYPE>> cellGroups = new HashSet<List<CELLTYPE>>(); + + /** + * Returns the cell at the given position in this row. + * + * @param index + * the position of the cell + * @return the cell at the index + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public CELLTYPE getCell(int index) { + return cells.get(index); + } + + /** + * Merges cells in a row + * + * @param cells + * The cells to be merged + * @return The first cell of the merged cells + */ + protected CELLTYPE join(List<CELLTYPE> cells) { + assert cells.size() > 1 : "You cannot merge less than 2 cells together"; + + // Ensure no cell is already grouped + for (CELLTYPE cell : cells) { + if (getCellGroupForCell(cell) != null) { + throw new IllegalStateException("Cell " + cell.getText() + + " is already grouped."); + } + } + + // Ensure continuous range + int firstCellIndex = this.cells.indexOf(cells.get(0)); + for (int i = 0; i < cells.size(); i++) { + if (this.cells.get(firstCellIndex + i) != cells.get(i)) { + throw new IllegalStateException( + "Cell range must be a continous range"); + } + } + + // Create a new group + cellGroups.add(new ArrayList<CELLTYPE>(cells)); + + // Calculates colspans, triggers refresh on section implicitly + calculateColspans(); + + // Returns first cell of group + return cells.get(0); + } + + /** + * Merges columns cells in a row + * + * @param columns + * The columns which header should be merged + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(GridColumn<?, ?>... columns) { + assert columns.length > 1 : "You cannot merge less than 2 columns together"; + + // Convert columns to cells + List<CELLTYPE> cells = new ArrayList<CELLTYPE>(); + for (GridColumn<?, ?> c : columns) { + int index = getSection().getGrid().getColumns().indexOf(c); + cells.add(this.cells.get(index)); + } + + return join(cells); + } + + /** + * Merges columns cells in a row + * + * @param cells + * The cells to merge. Must be from the same row. + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(CELLTYPE... cells) { + return join(Arrays.asList(cells)); + } + + private List<CELLTYPE> getCellGroupForCell(CELLTYPE cell) { + for (List<CELLTYPE> group : cellGroups) { + if (group.contains(cell)) { + return group; + } + } + return null; + } + + void calculateColspans() { + + // Reset all cells + for (CELLTYPE cell : cells) { + cell.setColspan(1); + } + + // Set colspan for grouped cells + for (List<CELLTYPE> group : cellGroups) { + + int firstVisibleColumnInGroup = -1; + int lastVisibleColumnInGroup = -1; + int hiddenInsideGroup = 0; + + /* + * To be able to calculate the colspan correctly we need to two + * things; find the first visible cell in the group which will + * get the colspan assigned to and find the amount of columns + * which should be spanned. + * + * To do that we iterate through all cells, marking into memory + * when we find the first visible cell, when we find the last + * visible cell and how many cells are hidden in between. + */ + for (int i = 0; i < group.size(); i++) { + CELLTYPE cell = group.get(i); + int cellIndex = this.cells.indexOf(cell); + boolean columnVisible = getSection().getGrid() + .getColumn(cellIndex).isVisible(); + if (columnVisible) { + lastVisibleColumnInGroup = i; + if (firstVisibleColumnInGroup == -1) { + firstVisibleColumnInGroup = i; + } + } else if (firstVisibleColumnInGroup != -1) { + hiddenInsideGroup++; + } + } + + if (firstVisibleColumnInGroup == -1 + || lastVisibleColumnInGroup == -1 + || firstVisibleColumnInGroup == lastVisibleColumnInGroup) { + // No cells in group + continue; + } + + /* + * Assign colspan to first cell in group. + */ + CELLTYPE firstVisibleCell = group + .get(firstVisibleColumnInGroup); + firstVisibleCell.setColspan(lastVisibleColumnInGroup + - firstVisibleColumnInGroup - hiddenInsideGroup + 1); + } + + } + + protected void addCell(int index) { + CELLTYPE cell = createCell(); + cell.setSection(getSection()); + cells.add(index, cell); + } + + protected void removeCell(int index) { + cells.remove(index); + } + + protected abstract CELLTYPE createCell(); + + protected GridStaticSection<?> getSection() { + return section; + } + + protected void setSection(GridStaticSection<?> section) { + this.section = section; + } + } + + private Grid<?> grid; + + private List<ROWTYPE> rows = new ArrayList<ROWTYPE>(); + + private boolean visible = true; + + /** + * Creates and returns a new instance of the row type. + * + * @return the created row + */ + protected abstract ROWTYPE createRow(); + + /** + * Informs the grid that this section should be re-rendered. + * <p> + * <b>Note</b> that re-render means calling update() on each cell, + * preAttach()/postAttach()/preDetach()/postDetach() is not called as the + * cells are not removed from the DOM. + */ + protected abstract void requestSectionRefresh(); + + /** + * Sets the visibility of the whole section. + * + * @param visible + * true to show this section, false to hide + */ + public void setVisible(boolean visible) { + this.visible = visible; + requestSectionRefresh(); + } + + /** + * Returns the visibility of this section. + * + * @return true if visible, false otherwise. + */ + public boolean isVisible() { + return visible; + } + + /** + * Inserts a new row at the given position. + * + * @param index + * the position at which to insert the row + * @return the new row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public ROWTYPE addRow(int index) { + ROWTYPE row = createRow(); + row.setSection(this); + for (int i = 0; i < getGrid().getColumnCount(); ++i) { + row.addCell(i); + } + rows.add(index, row); + + requestSectionRefresh(); + return row; + } + + /** + * Adds a new row at the top of this section. + * + * @return the new row + */ + public ROWTYPE prependRow() { + return addRow(0); + } + + /** + * Adds a new row at the bottom of this section. + * + * @return the new row + */ + public ROWTYPE appendRow() { + return addRow(rows.size()); + } + + /** + * Removes the row at the given position. + * + * @param index + * the position of the row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public void removeRow(int index) { + rows.remove(index); + requestSectionRefresh(); + } + + /** + * Removes the given row from the section. + * + * @param row + * the row to be removed + * + * @throws IllegalArgumentException + * if the row does not exist in this section + */ + public void removeRow(ROWTYPE row) { + try { + removeRow(rows.indexOf(row)); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "Section does not contain the given row"); + } + } + + /** + * Returns the row at the given position. + * + * @param index + * the position of the row + * @return the row with the given index + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public ROWTYPE getRow(int index) { + try { + return rows.get(index); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException("Row with index " + index + + " does not exist"); + } + } + + /** + * Returns the number of rows in this section. + * + * @return the number of rows + */ + public int getRowCount() { + return rows.size(); + } + + protected List<ROWTYPE> getRows() { + return rows; + } + + protected int getVisibleRowCount() { + return isVisible() ? getRowCount() : 0; + } + + protected void addColumn(GridColumn<?, ?> column, int index) { + for (ROWTYPE row : rows) { + row.addCell(index); + } + } + + protected void removeColumn(int index) { + for (ROWTYPE row : rows) { + row.removeCell(index); + } + } + + protected void setGrid(Grid<?> grid) { + this.grid = grid; + } + + protected Grid<?> getGrid() { + assert grid != null; + return grid; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/PositionFunction.java b/client/src/com/vaadin/client/ui/grid/PositionFunction.java new file mode 100644 index 0000000000..4db5efd0fc --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/PositionFunction.java @@ -0,0 +1,118 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Unit; + +/** + * A functional interface that can be used for positioning elements in the DOM. + * + * @since + * @author Vaadin Ltd + */ +interface PositionFunction { + /** + * A position function using "transform: translate3d(x,y,z)" to position + * elements in the DOM. + */ + public static class Translate3DPosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setProperty("transform", + "translate3d(" + x + "px, " + y + "px, 0)"); + } + + @Override + public void reset(Element e) { + e.getStyle().clearProperty("transform"); + } + } + + /** + * A position function using "transform: translate(x,y)" to position + * elements in the DOM. + */ + public static class TranslatePosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setProperty("transform", + "translate(" + x + "px," + y + "px)"); + } + + @Override + public void reset(Element e) { + e.getStyle().clearProperty("transform"); + } + } + + /** + * A position function using "-webkit-transform: translate3d(x,y,z)" to + * position elements in the DOM. + */ + public static class WebkitTranslate3DPosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setProperty("webkitTransform", + "translate3d(" + x + "px," + y + "px,0)"); + } + + @Override + public void reset(Element e) { + e.getStyle().clearProperty("webkitTransform"); + } + } + + /** + * A position function using "left: x" and "top: y" to position elements in + * the DOM. + */ + public static class AbsolutePosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setLeft(x, Unit.PX); + e.getStyle().setTop(y, Unit.PX); + } + + @Override + public void reset(Element e) { + e.getStyle().clearLeft(); + e.getStyle().clearTop(); + } + } + + /** + * Position an element in an (x,y) coordinate system in the DOM. + * + * @param e + * the element to position. Never <code>null</code>. + * @param x + * the x coordinate, in pixels + * @param y + * the y coordinate, in pixels + */ + void set(Element e, double x, double y); + + /** + * Resets any previously applied positioning, clearing the used style + * attributes. + * + * @param e + * the element for which to reset the positioning + */ + void reset(Element e); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/Renderer.java b/client/src/com/vaadin/client/ui/grid/Renderer.java new file mode 100644 index 0000000000..787a145326 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Renderer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + + +/** + * Renderer for rending a value <T> into cell. + * <p> + * You can add a renderer to any column by overring the + * {@link GridColumn#getRenderer()} method and returning your own renderer. You + * can retrieve the cell element using {@link Cell#getElement()}. + * + * @param <T> + * The column type + * + * @since + * @author Vaadin Ltd + */ +public interface Renderer<T> { + + /** + * Called whenever the {@link Grid} updates a cell + * + * @param cell + * The cell. Note that the cell is a flyweight and should not be + * stored outside of the method as it will change. + * + * @param data + * The column data object + */ + void render(FlyweightCell cell, T data); +} diff --git a/client/src/com/vaadin/client/ui/grid/Row.java b/client/src/com/vaadin/client/ui/grid/Row.java new file mode 100644 index 0000000000..6419a98574 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Row.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.TableRowElement; + +/** + * A representation of a row in an {@link Escalator}. + * + * @since + * @author Vaadin Ltd + */ +public interface Row { + /** + * Gets the row index. + * + * @return the row index + */ + public int getRow(); + + /** + * Gets the root element for this row. + * <p> + * The {@link EscalatorUpdater} may update the class names of the element + * and add inline styles, but may not modify the contained DOM structure. + * <p> + * If you wish to modify the cells within this row element, access them via + * the <code>List<{@link Cell}></code> objects passed in to + * {@code EscalatorUpdater.updateCells(Row, List)} + * + * @return the root element of the row + */ + public TableRowElement getElement(); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/RowContainer.java b/client/src/com/vaadin/client/ui/grid/RowContainer.java new file mode 100644 index 0000000000..d0fb0db103 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowContainer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; + +/** + * A representation of the rows in each of the sections (header, body and + * footer) in an {@link Escalator}. + * + * @since + * @author Vaadin Ltd + * @see Escalator#getHeader() + * @see Escalator#getBody() + * @see Escalator#getFooter() + */ +public interface RowContainer { + + /** + * An arbitrary pixel height of a row, before any autodetection for the row + * height has been made. + * */ + public static final int INITIAL_DEFAULT_ROW_HEIGHT = 20; + + /** + * Returns the current {@link EscalatorUpdater} used to render cells. + * + * @return the current escalator updater + */ + public EscalatorUpdater getEscalatorUpdater(); + + /** + * Sets the {@link EscalatorUpdater} to use when displaying data in the + * escalator. + * + * @param escalatorUpdater + * the escalator updater to use to render cells. May not be + * <code>null</code> + * @throws IllegalArgumentException + * if {@code cellRenderer} is <code>null</code> + * @see EscalatorUpdater#NULL + */ + public void setEscalatorUpdater(EscalatorUpdater escalatorUpdater) + throws IllegalArgumentException; + + /** + * Removes rows at a certain index in the current row container. + * + * @param index + * the index of the first row to be removed + * @param numberOfRows + * the number of rows to remove, starting from the index + * @throws IndexOutOfBoundsException + * if any integer number in the range + * <code>[index..(index+numberOfRows)]</code> is not an existing + * row index + * @throws IllegalArgumentException + * if {@code numberOfRows} is less than 1. + */ + public void removeRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Adds rows at a certain index in this row container. + * <p> + * The new rows will be inserted between the row at the index, and the row + * before (an index of 0 means that the rows are inserted at the beginning). + * Therefore, the rows currently at the index and afterwards will be moved + * downwards. + * <p> + * The contents of the inserted rows will subsequently be queried from the + * escalator updater. + * <p> + * <em>Note:</em> Only the contents of the inserted rows will be rendered. + * If inserting new rows affects the contents of existing rows, + * {@link #refreshRows(int, int)} needs to be called for those rows + * separately. + * + * @param index + * the index of the row before which new rows are inserted, or + * {@link #getRowCount()} to add rows at the end + * @param numberOfRows + * the number of rows to insert after the <code>index</code> + * @see #setEscalatorUpdater(EscalatorUpdater) + * @throws IndexOutOfBoundsException + * if <code>index</code> is not an integer in the range + * <code>[0..{@link #getRowCount()}]</code> + * @throws IllegalArgumentException + * if {@code numberOfRows} is less than 1. + */ + public void insertRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Refreshes a range of rows in the current row container. + * <p> + * The data for the refreshed rows are queried from the current cell + * renderer. + * + * @param index + * the index of the first row that will be updated + * @param numberOfRows + * the number of rows to update, starting from the index + * @see #setEscalatorUpdater(EscalatorUpdater) + * @throws IndexOutOfBoundsException + * if any integer number in the range + * <code>[index..(index+numberOfColumns)]</code> is not an + * existing column index. + * @throws IllegalArgumentException + * if {@code numberOfRows} is less than 1. + */ + public void refreshRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Gets the number of rows in the current row container. + * + * @return the number of rows in the current row container + */ + public int getRowCount(); + + /** + * The default height of the rows in this RowContainer. + * + * @param px + * the default height in pixels of the rows in this RowContainer + * @throws IllegalArgumentException + * if <code>px < 1</code> + * @see #getDefaultRowHeight() + */ + public void setDefaultRowHeight(int px) throws IllegalArgumentException; + + /** + * Returns the default height of the rows in this RowContainer. + * <p> + * This value will be equal to {@link #INITIAL_DEFAULT_ROW_HEIGHT} if the + * {@link Escalator} has not yet had a chance to autodetect the row height, + * or no explicit value has yet given via {@link #setDefaultRowHeight(int)} + * + * @return the default height of the rows in this RowContainer, in pixels + * @see #setDefaultRowHeight(int) + */ + public int getDefaultRowHeight(); + + /** + * Returns the cell object which contains information about the cell the + * element is in. + * + * @param element + * The element to get the cell for. If element is not present in + * row container then <code>null</code> is returned. + * + * @return the cell of the element, or <code>null</code> if element is not + * present in the {@link RowContainer}. + */ + public Cell getCell(Element element); + + /** + * Gets the row element with given logical index. For lazy loaded containers + * such as Escalators BodyRowContainer visibility should be checked before + * calling this function. See {@link Escalator#getVisibleRowRange()}. + * + * @param index + * the logical index of the element to retrieve + * @return the element at position {@code index} + * @throws IndexOutOfBoundsException + * if {@code index} is not valid within container + * @throws IllegalStateException + * if {@code index} is currently not available in the DOM + */ + public Element getRowElement(int index) throws IndexOutOfBoundsException, + IllegalStateException; + + /** + * Returns the root element of RowContainer + * + * @return RowContainer root element + */ + public Element getElement(); +} diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java new file mode 100644 index 0000000000..c5c5e45ca8 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import com.google.gwt.event.shared.GwtEvent; + +/** + * Event fired when the range of visible rows changes e.g. because of scrolling. + * + * @since + * @author Vaadin Ltd + */ +public class RowVisibilityChangeEvent extends + GwtEvent<RowVisibilityChangeHandler> { + /** + * The type of this event. + */ + public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>(); + + private final int firstVisibleRow; + private final int visibleRowCount; + + /** + * Creates a new row visibility change event + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of visible rows + */ + public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) { + this.firstVisibleRow = firstVisibleRow; + this.visibleRowCount = visibleRowCount; + } + + /** + * Gets the index of the first row that is at least partially visible. + * + * @return the index of the first visible row + */ + public int getFirstVisibleRow() { + return firstVisibleRow; + } + + /** + * Gets the number of at least partially visible rows. + * + * @return the number of visible rows + */ + public int getVisibleRowCount() { + return visibleRowCount; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType() + */ + @Override + public Type<RowVisibilityChangeHandler> getAssociatedType() { + return TYPE; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared + * .EventHandler) + */ + @Override + protected void dispatch(RowVisibilityChangeHandler handler) { + handler.onRowVisibilityChange(this); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java new file mode 100644 index 0000000000..6aa165fe04 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Event handler that gets notified when the range of visible rows changes e.g. + * because of scrolling. + * + * @since + * @author Vaadin Ltd + */ +public interface RowVisibilityChangeHandler extends EventHandler { + + /** + * Called when the range of visible rows changes e.g. because of scrolling. + * + * @param event + * the row visibility change event describing the change + */ + void onRowVisibilityChange(RowVisibilityChangeEvent event); + +} diff --git a/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java new file mode 100644 index 0000000000..5a54f5fc68 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java @@ -0,0 +1,639 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.shared.EventHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.event.shared.HandlerManager; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.EventListener; +import com.google.gwt.user.client.Timer; + +/** + * An element-like bundle representing a configurable and visual scrollbar in + * one axis. + * + * @since + * @author Vaadin Ltd + * @see VerticalScrollbarBundle + * @see HorizontalScrollbarBundle + */ +abstract class ScrollbarBundle { + + private class TemporaryResizer extends Object { + private static final int TEMPORARY_RESIZE_DELAY = 1000; + + private final Timer timer = new Timer() { + @Override + public void run() { + internalSetScrollbarThickness(1); + } + }; + + public void show() { + internalSetScrollbarThickness(OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX); + timer.schedule(TEMPORARY_RESIZE_DELAY); + } + } + + /** + * A means to listen to when the scrollbar handle in a + * {@link ScrollbarBundle} either appears or is removed. + */ + public interface VisibilityHandler extends EventHandler { + /** + * This method is called whenever the scrollbar handle's visibility is + * changed in a {@link ScrollbarBundle}. + * + * @param event + * the {@link VisibilityChangeEvent} + */ + void visibilityChanged(VisibilityChangeEvent event); + } + + public static class VisibilityChangeEvent extends + GwtEvent<VisibilityHandler> { + public static final Type<VisibilityHandler> TYPE = new Type<ScrollbarBundle.VisibilityHandler>() { + @Override + public String toString() { + return "VisibilityChangeEvent"; + } + }; + + private final boolean isScrollerVisible; + + private VisibilityChangeEvent(boolean isScrollerVisible) { + this.isScrollerVisible = isScrollerVisible; + } + + /** + * Checks whether the scroll handle is currently visible or not + * + * @return <code>true</code> if the scroll handle is currently visible. + * <code>false</code> if not. + */ + public boolean isScrollerVisible() { + return isScrollerVisible; + } + + @Override + public Type<VisibilityHandler> getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(VisibilityHandler handler) { + handler.visibilityChanged(this); + } + } + + /** + * The pixel size for OSX's invisible scrollbars. + * <p> + * Touch devices don't show a scrollbar at all, so the scrollbar size is + * irrelevant in their case. There doesn't seem to be any other popular + * platforms that has scrollbars similar to OSX. Thus, this behavior is + * tailored for OSX only, until additional platforms start behaving this + * way. + */ + private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13; + + /** + * The allowed value inaccuracy when comparing two double-typed pixel + * values. + * <p> + * Since we're comparing pixels on a screen, epsilon must be less than 1. + * 0.49 was deemed a perfectly fine and beautifully round number. + */ + private static final double PIXEL_EPSILON = 0.49d; + + /** + * A representation of a single vertical scrollbar. + * + * @see VerticalScrollbarBundle#getElement() + */ + final static class VerticalScrollbarBundle extends ScrollbarBundle { + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + root.addClassName(primaryStyleName + "-scroller-vertical"); + } + + @Override + protected void internalSetScrollPos(int px) { + root.setScrollTop(px); + } + + @Override + protected int internalGetScrollPos() { + return root.getScrollTop(); + } + + @Override + protected void internalSetScrollSize(int px) { + scrollSizeElement.getStyle().setHeight(px, Unit.PX); + } + + @Override + protected int internalGetScrollSize() { + return scrollSizeElement.getOffsetHeight(); + } + + @Override + protected void internalSetOffsetSize(double px) { + root.getStyle().setHeight(px, Unit.PX); + } + + @Override + public double getOffsetSize() { + return root.getOffsetHeight(); + } + + @Override + protected void internalSetScrollbarThickness(int px) { + root.getStyle().setWidth(px, Unit.PX); + scrollSizeElement.getStyle().setWidth(px, Unit.PX); + } + + @Override + protected int internalGetScrollbarThickness() { + return root.getOffsetWidth(); + } + + @Override + protected void forceScrollbar(boolean enable) { + if (enable) { + root.getStyle().setOverflowY(Overflow.SCROLL); + } else { + root.getStyle().clearOverflowY(); + } + } + } + + /** + * A representation of a single horizontal scrollbar. + * + * @see HorizontalScrollbarBundle#getElement() + */ + final static class HorizontalScrollbarBundle extends ScrollbarBundle { + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + root.addClassName(primaryStyleName + "-scroller-horizontal"); + } + + @Override + protected void internalSetScrollPos(int px) { + root.setScrollLeft(px); + } + + @Override + protected int internalGetScrollPos() { + return root.getScrollLeft(); + } + + @Override + protected void internalSetScrollSize(int px) { + scrollSizeElement.getStyle().setWidth(px, Unit.PX); + } + + @Override + protected int internalGetScrollSize() { + return scrollSizeElement.getOffsetWidth(); + } + + @Override + protected void internalSetOffsetSize(double px) { + root.getStyle().setWidth(px, Unit.PX); + } + + @Override + public double getOffsetSize() { + return root.getOffsetWidth(); + } + + @Override + protected void internalSetScrollbarThickness(int px) { + root.getStyle().setHeight(px, Unit.PX); + scrollSizeElement.getStyle().setHeight(px, Unit.PX); + } + + @Override + protected int internalGetScrollbarThickness() { + return root.getOffsetHeight(); + } + + @Override + protected void forceScrollbar(boolean enable) { + if (enable) { + root.getStyle().setOverflowX(Overflow.SCROLL); + } else { + root.getStyle().clearOverflowX(); + } + } + } + + protected final Element root = DOM.createDiv(); + protected final Element scrollSizeElement = DOM.createDiv(); + protected boolean isInvisibleScrollbar = false; + + private double scrollPos = 0; + private double maxScrollPos = 0; + + private boolean scrollHandleIsVisible = false; + + /** @deprecarted access via {@link #getHandlerManager()} instead. */ + @Deprecated + private HandlerManager handlerManager; + + private TemporaryResizer invisibleScrollbarTemporaryResizer = new TemporaryResizer(); + + private ScrollbarBundle() { + root.appendChild(scrollSizeElement); + } + + protected abstract int internalGetScrollSize(); + + /** + * Sets the primary style name + * + * @param primaryStyleName + * The primary style name to use + */ + public void setStylePrimaryName(String primaryStyleName) { + root.setClassName(primaryStyleName + "-scroller"); + } + + /** + * Gets the root element of this scrollbar-composition. + * + * @return the root element + */ + public final Element getElement() { + return root; + } + + /** + * Modifies the scroll position of this scrollbar by a number of pixels. + * <p> + * <em>Note:</em> Even though {@code double} values are used, they are + * currently only used as integers as large {@code int} (or small but fast + * {@code long}). This means, all values are truncated to zero decimal + * places. + * + * @param delta + * the delta in pixels to change the scroll position by + */ + public final void setScrollPosByDelta(double delta) { + if (delta != 0) { + setScrollPos(getScrollPos() + delta); + } + } + + /** + * Modifies {@link #root root's} dimensions in the axis the scrollbar is + * representing. + * + * @param px + * the new size of {@link #root} in the dimension this scrollbar + * is representing + */ + protected abstract void internalSetOffsetSize(double px); + + /** + * Sets the length of the scrollbar. + * <p> + * <em>Note:</em> Even though {@code double} values are used, they are + * currently only used as integers as large {@code int} (or small but fast + * {@code long}). This means, all values are truncated to zero decimal + * places. + * + * @param px + * the length of the scrollbar in pixels + */ + public final void setOffsetSize(double px) { + internalSetOffsetSize(Math.max(0, truncate(px))); + forceScrollbar(showsScrollHandle()); + recalculateMaxScrollPos(); + fireVisibilityChangeIfNeeded(); + } + + /** + * Force the scrollbar to be visible with CSS. In practice, this means to + * set either <code>overflow-x</code> or <code>overflow-y</code> to " + * <code>scroll</code>" in the scrollbar's direction. + * <p> + * This is an IE8 workaround, since it doesn't always show scrollbars with + * <code>overflow: auto</code> enabled. + */ + protected abstract void forceScrollbar(boolean enable); + + /** + * Gets the length of the scrollbar + * + * @return the length of the scrollbar in pixels + */ + public abstract double getOffsetSize(); + + /** + * Sets the scroll position of the scrollbar in the axis the scrollbar is + * representing. + * <p> + * <em>Note:</em> Even though {@code double} values are used, they are + * currently only used as integers as large {@code int} (or small but fast + * {@code long}). This means, all values are truncated to zero decimal + * places. + * + * @param px + * the new scroll position in pixels + */ + public final void setScrollPos(double px) { + double oldScrollPos = scrollPos; + scrollPos = Math.max(0, Math.min(maxScrollPos, truncate(px))); + + if (!pixelValuesEqual(oldScrollPos, scrollPos)) { + if (isInvisibleScrollbar) { + invisibleScrollbarTemporaryResizer.show(); + } + + /* + * This is where the value needs to be converted into an integer no + * matter how we flip it, since GWT expects an integer value. + * There's no point making a JSNI method that accepts doubles as the + * scroll position, since the browsers themselves don't support such + * large numbers (as of today, 25.3.2014). This double-ranged is + * only facilitating future virtual scrollbars. + */ + internalSetScrollPos(toInt32(scrollPos)); + } + } + + /** + * Truncates a double such that no decimal places are retained. + * <p> + * E.g. {@code trunc(2.3d) == 2.0d} and {@code trunc(-2.3d) == -2.0d}. + * + * @param num + * the double value to be truncated + * @return the {@code num} value without any decimal digits + */ + private static double truncate(double num) { + if (num > 0) { + return Math.floor(num); + } else { + return Math.ceil(num); + } + } + + /** + * Modifies the element's scroll position (scrollTop or scrollLeft). + * <p> + * <em>Note:</em> The parameter here is a type of integer (instead of a + * double) by design. The browsers internally convert all double values into + * an integer value. To make this fact explicit, this API has chosen to + * force integers already at this level. + * + * @param px + * integer pixel value to scroll to + */ + protected abstract void internalSetScrollPos(int px); + + /** + * Gets the scroll position of the scrollbar in the axis the scrollbar is + * representing. + * + * @return the new scroll position in pixels + */ + public final double getScrollPos() { + assert internalGetScrollPos() == toInt32(scrollPos) : "calculated scroll position (" + + toInt32(scrollPos) + + ") did not match the DOM element scroll position (" + + internalGetScrollPos() + ")"; + return scrollPos; + } + + /** + * Retrieves the element's scroll position (scrollTop or scrollLeft). + * <p> + * <em>Note:</em> The parameter here is a type of integer (instead of a + * double) by design. The browsers internally convert all double values into + * an integer value. To make this fact explicit, this API has chosen to + * force integers already at this level. + * + * @return integer pixel value of the scroll position + */ + protected abstract int internalGetScrollPos(); + + /** + * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in + * such a way that the scrollbar is able to scroll a certain number of + * pixels in the axis it is representing. + * + * @param px + * the new size of {@link #scrollSizeElement} in the dimension + * this scrollbar is representing + */ + protected abstract void internalSetScrollSize(int px); + + /** + * Sets the amount of pixels the scrollbar needs to be able to scroll + * through. + * <p> + * <em>Note:</em> Even though {@code double} values are used, they are + * currently only used as integers as large {@code int} (or small but fast + * {@code long}). This means, all values are truncated to zero decimal + * places. + * + * @param px + * the number of pixels the scrollbar should be able to scroll + * through + */ + public final void setScrollSize(double px) { + internalSetScrollSize(toInt32(Math.max(0, truncate(px)))); + forceScrollbar(showsScrollHandle()); + recalculateMaxScrollPos(); + fireVisibilityChangeIfNeeded(); + } + + /** + * Gets the amount of pixels the scrollbar needs to be able to scroll + * through. + * + * @return the number of pixels the scrollbar should be able to scroll + * through + */ + public double getScrollSize() { + return internalGetScrollSize(); + } + + /** + * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the + * opposite axis to what the scrollbar is representing. + * + * @param px + * the dimension that {@link #scrollSizeElement} should take in + * the opposite axis to what the scrollbar is representing + */ + protected abstract void internalSetScrollbarThickness(int px); + + /** + * Sets the scrollbar's thickness. + * <p> + * If the thickness is set to 0, the scrollbar will be treated as an + * "invisible" scrollbar. This means, the DOM structure will be given a + * non-zero size, but {@link #getScrollbarThickness()} will still return the + * value 0. + * + * @param px + * the scrollbar's thickness in pixels + */ + public final void setScrollbarThickness(int px) { + isInvisibleScrollbar = (px == 0); + + if (isInvisibleScrollbar) { + Event.sinkEvents(root, Event.ONSCROLL); + Event.setEventListener(root, new EventListener() { + @Override + public void onBrowserEvent(Event event) { + invisibleScrollbarTemporaryResizer.show(); + } + }); + } else { + Event.sinkEvents(root, 0); + Event.setEventListener(root, null); + } + + internalSetScrollbarThickness(Math.max(1, px)); + } + + /** + * Gets the scrollbar's thickness as defined in the DOM. + * + * @return the scrollbar's thickness as defined in the DOM, in pixels + */ + protected abstract int internalGetScrollbarThickness(); + + /** + * Gets the scrollbar's thickness. + * <p> + * This value will differ from the value in the DOM, if the thickness was + * set to 0 with {@link #setScrollbarThickness(int)}, as the scrollbar is + * then treated as "invisible." + * + * @return the scrollbar's thickness in pixels + */ + public final int getScrollbarThickness() { + if (!isInvisibleScrollbar) { + return internalGetScrollbarThickness(); + } else { + return 0; + } + } + + /** + * Checks whether the scrollbar's handle is visible. + * <p> + * In other words, this method checks whether the contents is larger than + * can visually fit in the element. + * + * @return <code>true</code> iff the scrollbar's handle is visible + */ + public boolean showsScrollHandle() { + return getOffsetSize() < getScrollSize(); + } + + public void recalculateMaxScrollPos() { + double scrollSize = getScrollSize(); + double offsetSize = getOffsetSize(); + maxScrollPos = Math.max(0, scrollSize - offsetSize); + + // make sure that the correct max scroll position is maintained. + setScrollPos(scrollPos); + } + + /** + * This is a method that JSNI can call to synchronize the object state from + * the DOM. + */ + private final void updateScrollPosFromDom() { + scrollPos = internalGetScrollPos(); + } + + protected HandlerManager getHandlerManager() { + if (handlerManager == null) { + handlerManager = new HandlerManager(this); + } + return handlerManager; + } + + /** + * Adds handler for the scrollbar handle visibility. + * + * @param handler + * the {@link VisibilityHandler} to add + * @return {@link HandlerRegistration} used to remove the handler + */ + public HandlerRegistration addVisibilityHandler( + final VisibilityHandler handler) { + return getHandlerManager().addHandler(VisibilityChangeEvent.TYPE, + handler); + } + + private void fireVisibilityChangeIfNeeded() { + final boolean oldHandleIsVisible = scrollHandleIsVisible; + scrollHandleIsVisible = showsScrollHandle(); + if (oldHandleIsVisible != scrollHandleIsVisible) { + final VisibilityChangeEvent event = new VisibilityChangeEvent( + scrollHandleIsVisible); + getHandlerManager().fireEvent(event); + } + } + + /** + * Converts a double into an integer by JavaScript's terms. + * <p> + * Implementation copied from {@link Element#toInt32(double)}. + * + * @param val + * the double value to convert into an integer + * @return the double value converted to an integer + */ + private static native int toInt32(double val) + /*-{ + return val | 0; + }-*/; + + /** + * Compares two double values with the error margin of + * {@link #PIXEL_EPSILON} (i.e. {@value #PIXEL_EPSILON}) + * + * @param num1 + * the first value for which to compare equality + * @param num2 + * the second value for which to compare equality + */ + private static boolean pixelValuesEqual(final double num1, final double num2) { + return Math.abs(num1 - num2) <= PIXEL_EPSILON; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java new file mode 100644 index 0000000000..ef021a496a --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java @@ -0,0 +1,438 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.datasources; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.data.DataSource; +import com.vaadin.shared.util.SharedUtil; + +/** + * A simple list based on an in-memory data source for simply adding a list of + * row pojos to the grid. Based on a wrapped list instance which supports adding + * and removing of items. + * + * <p> + * Usage: + * + * <pre> + * ListDataSource<Integer> ds = new ListDataSource<Integer>(1, 2, 3, 4); + * + * // Add item to the data source + * ds.asList().add(5); + * + * // Remove item from the data source + * ds.asList().remove(3); + * + * // Add multiple items + * ds.asList().addAll(Arrays.asList(5, 6, 7)); + * </pre> + * + * @since + * @author Vaadin Ltd + */ +public class ListDataSource<T> implements DataSource<T> { + + private class RowHandleImpl extends RowHandle<T> { + + private final T row; + + public RowHandleImpl(T row) { + this.row = row; + } + + @Override + public T getRow() { + /* + * We'll cheat here and don't throw an IllegalStateException even if + * this isn't pinned, because we know that the reference never gets + * stale. + */ + return row; + } + + @Override + public void pin() { + // NOOP, really + } + + @Override + public void unpin() throws IllegalStateException { + /* + * Just to make things easier for everyone, we won't throw the + * exception, even in illegal situations. + */ + } + + @Override + protected boolean equalsExplicit(Object obj) { + if (obj instanceof ListDataSource.RowHandleImpl) { + /* + * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I + * like the @SuppressWarnings more (keeps the line length in + * check.) + */ + @SuppressWarnings("unchecked") + RowHandleImpl rhi = (RowHandleImpl) obj; + return SharedUtil.equals(row, rhi.row); + } else { + return false; + } + } + + @Override + protected int hashCodeExplicit() { + return row.hashCode(); + } + } + + /** + * Wraps the datasource list and notifies the change handler of changing to + * the list + */ + private class ListWrapper implements List<T> { + + @Override + public int size() { + return ds.size(); + } + + @Override + public boolean isEmpty() { + return ds.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return contains(o); + } + + @Override + public Iterator<T> iterator() { + return new ListWrapperIterator(ds.iterator()); + } + + @Override + public Object[] toArray() { + return ds.toArray(); + } + + @Override + @SuppressWarnings("hiding") + public <T> T[] toArray(T[] a) { + return toArray(a); + } + + @Override + public boolean add(T e) { + if (ds.add(e)) { + if (changeHandler != null) { + changeHandler.dataAdded(ds.size() - 1, 1); + } + return true; + } + return false; + } + + @Override + public boolean remove(Object o) { + int index = ds.indexOf(o); + if (ds.remove(o)) { + if (changeHandler != null) { + changeHandler.dataRemoved(index, 1); + } + return true; + } + return false; + } + + @Override + public boolean containsAll(Collection<?> c) { + return ds.containsAll(c); + } + + @Override + public boolean addAll(Collection<? extends T> c) { + int idx = ds.size(); + if (ds.addAll(c)) { + if (changeHandler != null) { + changeHandler.dataAdded(idx, c.size()); + } + return true; + } + return false; + } + + @Override + public boolean addAll(int index, Collection<? extends T> c) { + if (ds.addAll(index, c)) { + if (changeHandler != null) { + changeHandler.dataAdded(index, c.size()); + } + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection<?> c) { + if (ds.removeAll(c)) { + if (changeHandler != null) { + // Have to update the whole list as the removal does not + // have to be a continuous range + changeHandler.dataUpdated(0, ds.size()); + changeHandler.dataAvailable(0, ds.size()); + } + return true; + } + return false; + } + + @Override + public boolean retainAll(Collection<?> c) { + if (ds.retainAll(c)) { + if (changeHandler != null) { + // Have to update the whole list as the retain does not + // have to be a continuous range + changeHandler.dataUpdated(0, ds.size()); + changeHandler.dataAvailable(0, ds.size()); + } + return true; + } + return false; + } + + @Override + public void clear() { + int size = ds.size(); + ds.clear(); + if (changeHandler != null) { + changeHandler.dataRemoved(0, size); + } + } + + @Override + public T get(int index) { + return ds.get(index); + } + + @Override + public T set(int index, T element) { + T prev = ds.set(index, element); + if (changeHandler != null) { + changeHandler.dataUpdated(index, 1); + } + return prev; + } + + @Override + public void add(int index, T element) { + ds.add(index, element); + if (changeHandler != null) { + changeHandler.dataAdded(index, 1); + } + } + + @Override + public T remove(int index) { + T removed = ds.remove(index); + if (changeHandler != null) { + changeHandler.dataRemoved(index, 1); + } + return removed; + } + + @Override + public int indexOf(Object o) { + return ds.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return ds.lastIndexOf(o); + } + + @Override + public ListIterator<T> listIterator() { + // TODO could be implemented by a custom iterator. + throw new UnsupportedOperationException( + "List iterators not supported at this time."); + } + + @Override + public ListIterator<T> listIterator(int index) { + // TODO could be implemented by a custom iterator. + throw new UnsupportedOperationException( + "List iterators not supported at this time."); + } + + @Override + public List<T> subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException("Sub lists not supported."); + } + } + + /** + * Iterator returned by {@link ListWrapper} + */ + private class ListWrapperIterator implements Iterator<T> { + + private final Iterator<T> iterator; + + /** + * Constructs a new iterator + */ + public ListWrapperIterator(Iterator<T> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "Iterator.remove() is not supported by this iterator."); + } + } + + /** + * Datasource for providing row pojo's + */ + private final List<T> ds; + + /** + * Wrapper that wraps the data source + */ + private final ListWrapper wrapper; + + /** + * Handler for listening to changes in the underlying list. + */ + private DataChangeHandler changeHandler; + + /** + * Constructs a new list data source. + * <p> + * Note: Modifications to the original list will not be reflected in the + * data source after the data source has been constructed. To add or remove + * items to the data source after it has been constructed use + * {@link ListDataSource#asList()}. + * + * + * @param datasource + * The list to use for providing the data to the grid + */ + public ListDataSource(List<T> datasource) { + if (datasource == null) { + throw new IllegalArgumentException("datasource cannot be null"); + } + ds = new ArrayList<T>(datasource); + wrapper = new ListWrapper(); + } + + /** + * Constructs a data source with a set of rows. You can dynamically add and + * remove rows from the data source via the list you get from + * {@link ListDataSource#asList()} + * + * @param rows + * The rows to initially add to the data source + */ + public ListDataSource(T... rows) { + if (rows == null) { + ds = new ArrayList<T>(); + } else { + ds = new ArrayList<T>(Arrays.asList(rows)); + } + wrapper = new ListWrapper(); + } + + @Override + public void ensureAvailability(int firstRowIndex, int numberOfRows) { + if (firstRowIndex >= ds.size()) { + throw new IllegalStateException( + "Trying to fetch rows outside of array"); + } + changeHandler.dataAvailable(firstRowIndex, numberOfRows); + } + + @Override + public T getRow(int rowIndex) { + return ds.get(rowIndex); + } + + @Override + public int getEstimatedSize() { + return ds.size(); + } + + @Override + public void setDataChangeHandler(DataChangeHandler dataChangeHandler) { + this.changeHandler = dataChangeHandler; + } + + /** + * Gets the list that backs this datasource. Any changes made to this list + * will be reflected in the datasource. + * <p> + * Note: The list is not the same list as passed into the data source via + * the constructor. + * + * @return Returns a list implementation that wraps the real list that backs + * the data source and provides events for the data source + * listeners. + */ + public List<T> asList() { + return wrapper; + } + + @Override + public RowHandle<T> getHandle(T row) throws IllegalStateException { + assert ds.contains(row) : "This data source doesn't contain the row " + + row; + return new RowHandleImpl(row); + } + + /** + * Sort entire container according to a {@link Comparator}. + * + * @param comparator + * a comparator object, which compares two data source entries + * (beans/pojos) + */ + public void sort(Comparator<T> comparator) { + Collections.sort(ds, comparator); + if (changeHandler != null) { + changeHandler.dataUpdated(0, ds.size()); + } + } +} diff --git a/client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java b/client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java new file mode 100644 index 0000000000..9e643825e9 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java @@ -0,0 +1,177 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.datasources; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.GridColumn; +import com.vaadin.client.ui.grid.sort.SortEvent; +import com.vaadin.client.ui.grid.sort.SortEventHandler; +import com.vaadin.client.ui.grid.sort.SortOrder; +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Provides sorting facility from Grid for the {@link ListDataSource} in-memory + * data source. + * + * @since + * @author Vaadin Ltd + * @param <T> + * Grid row data type + */ +public class ListSorter<T> { + + private Grid<T> grid; + private Map<GridColumn<?, T>, Comparator<?>> comparators; + private HandlerRegistration sortHandlerRegistration; + + public ListSorter(Grid<T> grid) { + + if (grid == null) { + throw new IllegalArgumentException("Grid can not be null"); + } + + this.grid = grid; + comparators = new HashMap<GridColumn<?, T>, Comparator<?>>(); + + sortHandlerRegistration = grid + .addSortHandler(new SortEventHandler<T>() { + @Override + public void sort(SortEvent<T> event) { + ListSorter.this.sort(event.getOrder()); + } + }); + } + + /** + * Detach this Sorter from the Grid. This unregisters the sort event handler + * which was used to apply sorting to the ListDataSource. + */ + public void removeFromGrid() { + sortHandlerRegistration.removeHandler(); + } + + /** + * Assign or remove a comparator for a column. This comparator method, if + * defined, is always used in favour of 'natural' comparison of objects + * (i.e. the compareTo of objects implementing the Comparable interface, + * which includes all standard data classes like String, Number derivatives + * and Dates). Any existing comparator can be removed by passing in a + * non-null GridColumn and a null Comparator. + * + * @param column + * a grid column. May not be null. + * @param comparator + * comparator method for the values returned by the grid column. + * If null, any existing comparator is removed. + */ + public <C> void setComparator(GridColumn<C, T> column, + Comparator<C> comparator) { + if (column == null) { + throw new IllegalArgumentException( + "Column reference can not be null"); + } + if (comparator == null) { + comparators.remove(column); + } else { + comparators.put(column, comparator); + } + } + + /** + * Retrieve the comparator assigned for a specific grid column. + * + * @param column + * a grid column. May not be null. + * @return a comparator, or null if no comparator for the specified grid + * column has been set. + */ + @SuppressWarnings("unchecked") + public <C> Comparator<C> getComparator(GridColumn<C, T> column) { + if (column == null) { + throw new IllegalArgumentException( + "Column reference can not be null"); + } + return (Comparator<C>) comparators.get(column); + } + + /** + * Remove all comparator mappings. Useful if the data source has changed but + * this Sorter is being re-used. + */ + public void clearComparators() { + comparators.clear(); + } + + /** + * Apply sorting to the current ListDataSource. + * + * @param order + * the sort order list provided by the grid sort event + */ + private void sort(final List<SortOrder> order) { + DataSource<T> ds = grid.getDataSource(); + if (!(ds instanceof ListDataSource)) { + throw new IllegalStateException("Grid " + grid + + " data source is not a ListDataSource!"); + } + + ((ListDataSource<T>) ds).sort(new Comparator<T>() { + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public int compare(T a, T b) { + + for (SortOrder o : order) { + + GridColumn column = o.getColumn(); + Comparator cmp = ListSorter.this.comparators.get(column); + int result = 0; + Object value_a = column.getValue(a); + Object value_b = column.getValue(b); + if (cmp != null) { + result = cmp.compare(value_a, value_b); + } else { + if (!(value_a instanceof Comparable)) { + throw new IllegalStateException("Column " + column + + " has no assigned comparator and value " + + value_a + " isn't naturally comparable"); + } + result = ((Comparable) value_a).compareTo(value_b); + } + + if (result != 0) { + return o.getDirection() == SortDirection.ASCENDING ? result + : -result; + } + } + + if (order.size() > 0) { + return order.get(0).getDirection() == SortDirection.ASCENDING ? a + .hashCode() - b.hashCode() + : b.hashCode() - a.hashCode(); + } + return a.hashCode() - b.hashCode(); + } + }); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java new file mode 100644 index 0000000000..28a85924fa --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Base interface of all handlers for {@link AbstractGridKeyEvent}s. + * + * @since + * @author Vaadin Ltd + */ +public abstract interface AbstractGridKeyEventHandler extends EventHandler { + + public abstract interface GridKeyDownHandler<T> extends + AbstractGridKeyEventHandler { + public void onKeyDown(GridKeyDownEvent<T> event); + } + + public abstract interface GridKeyUpHandler<T> extends + AbstractGridKeyEventHandler { + public void onKeyUp(GridKeyUpEvent<T> event); + } + + public abstract interface GridKeyPressHandler<T> extends + AbstractGridKeyEventHandler { + public void onKeyPress(GridKeyPressEvent<T> event); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java new file mode 100644 index 0000000000..a3b76ea5d7 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler; + +/** + * Handler for {@link GridKeyDownEvent}s that happen when active cell is in the + * body of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface BodyKeyDownHandler<T> extends GridKeyDownHandler<T> { +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java new file mode 100644 index 0000000000..5548994cf9 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler; + +/** + * Handler for {@link GridKeyPressEvent}s that happen when active cell is in the + * body of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface BodyKeyPressHandler<T> extends GridKeyPressHandler<T> { +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java new file mode 100644 index 0000000000..33b4fc81fe --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler; + +/** + * Handler for {@link GridKeyUpEvent}s that happen when active cell is in the + * body of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface BodyKeyUpHandler<T> extends GridKeyUpHandler<T> { +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java new file mode 100644 index 0000000000..e90f52e736 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler; + +/** + * Handler for {@link GridKeyDownEvent}s that happen when active cell is in the + * footer of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface FooterKeyDownHandler<T> extends GridKeyDownHandler<T> { +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java new file mode 100644 index 0000000000..58f48f36f5 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler; + +/** + * Handler for {@link GridKeyPressEvent}s that happen when active cell is in the + * footer of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface FooterKeyPressHandler<T> extends GridKeyPressHandler<T> { +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java new file mode 100644 index 0000000000..d6bcddf710 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler; + +/** + * Handler for {@link GridKeyUpEvent}s that happen when active cell is in the + * footer of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface FooterKeyUpHandler<T> extends GridKeyUpHandler<T> { +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.java b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.java new file mode 100644 index 0000000000..8af65dbf49 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.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.ui.grid.keyevents; + +import com.google.gwt.dom.client.BrowserEvents; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler; + +/** + * Represents native key down event in Grid. + * + * @since + * @author Vaadin Ltd + */ +public class GridKeyDownEvent<T> extends + AbstractGridKeyEvent<T, GridKeyDownHandler<T>> { + + public GridKeyDownEvent(Grid<T> grid) { + super(grid); + } + + @Override + protected void dispatch(GridKeyDownHandler<T> handler) { + super.dispatch(handler); + if ((activeSection == GridSection.BODY && handler instanceof BodyKeyDownHandler) + || (activeSection == GridSection.HEADER && handler instanceof HeaderKeyDownHandler) + || (activeSection == GridSection.FOOTER && handler instanceof FooterKeyDownHandler)) { + handler.onKeyDown(this); + } + } + + @Override + protected String getBrowserEventType() { + return BrowserEvents.KEYDOWN; + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.java b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.java new file mode 100644 index 0000000000..6f06bc6674 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.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.ui.grid.keyevents; + +import com.google.gwt.dom.client.BrowserEvents; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler; + +/** + * Represents native key press event in Grid. + * + * @since + * @author Vaadin Ltd + */ +public class GridKeyPressEvent<T> extends + AbstractGridKeyEvent<T, GridKeyPressHandler<T>> { + + public GridKeyPressEvent(Grid<T> grid) { + super(grid); + } + + @Override + protected void dispatch(GridKeyPressHandler<T> handler) { + super.dispatch(handler); + if ((activeSection == GridSection.BODY && handler instanceof BodyKeyPressHandler) + || (activeSection == GridSection.HEADER && handler instanceof HeaderKeyPressHandler) + || (activeSection == GridSection.FOOTER && handler instanceof FooterKeyPressHandler)) { + handler.onKeyPress(this); + } + } + + @Override + protected String getBrowserEventType() { + return BrowserEvents.KEYPRESS; + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.java b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.java new file mode 100644 index 0000000000..d289dbae98 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.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.ui.grid.keyevents; + +import com.google.gwt.dom.client.BrowserEvents; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent; +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler; + +/** + * Represents native key up event in Grid. + * + * @since + * @author Vaadin Ltd + */ +public class GridKeyUpEvent<T> extends + AbstractGridKeyEvent<T, GridKeyUpHandler<T>> { + + public GridKeyUpEvent(Grid<T> grid) { + super(grid); + } + + @Override + protected void dispatch(GridKeyUpHandler<T> handler) { + super.dispatch(handler); + if ((activeSection == GridSection.BODY && handler instanceof BodyKeyUpHandler) + || (activeSection == GridSection.HEADER && handler instanceof HeaderKeyUpHandler) + || (activeSection == GridSection.FOOTER && handler instanceof FooterKeyUpHandler)) { + handler.onKeyUp(this); + } + } + + @Override + protected String getBrowserEventType() { + return BrowserEvents.KEYUP; + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java new file mode 100644 index 0000000000..28c9a8e056 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler; + +/** + * Handler for {@link GridKeyDownEvent}s that happen when active cell is in the + * header of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface HeaderKeyDownHandler<T> extends GridKeyDownHandler<T> { +} diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java new file mode 100644 index 0000000000..607c30493d --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler; + +/** + * Handler for {@link GridKeyPressEvent}s that happen when active cell is in the + * header of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface HeaderKeyPressHandler<T> extends GridKeyPressHandler<T> { +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java new file mode 100644 index 0000000000..bfa3dde79b --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.keyevents; + +import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler; + +/** + * Handler for {@link GridKeyUpEvent}s that happen when active cell is in the + * header of the Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface HeaderKeyUpHandler<T> extends GridKeyUpHandler<T> { +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java new file mode 100644 index 0000000000..cad5af97df --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java @@ -0,0 +1,156 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.google.gwt.json.client.JSONValue; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.Util; +import com.vaadin.client.communication.JsonDecoder; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Type; +import com.vaadin.client.metadata.TypeData; +import com.vaadin.client.metadata.TypeDataStore; +import com.vaadin.client.ui.grid.GridConnector; +import com.vaadin.client.ui.grid.Renderer; + +/** + * An abstract base class for renderer connectors. A renderer connector is used + * to link a client-side {@link Renderer} to a server-side + * {@link com.vaadin.ui.components.grid.Renderer Renderer}. As a connector, it + * can use the regular Vaadin RPC and shared state mechanism to pass additional + * state and information between the client and the server. This base class + * itself only uses the basic + * {@link com.vaadin.shared.communication.SharedState SharedState} and no RPC + * interfaces. + * + * @since + * @author Vaadin Ltd + */ +public abstract class AbstractRendererConnector<T> extends + AbstractExtensionConnector { + + private Renderer<T> renderer = null; + + private final Type presentationType = TypeDataStore + .getPresentationType(this.getClass()); + + protected AbstractRendererConnector() { + if (presentationType == null) { + throw new IllegalStateException( + "No presentation type found for " + + Util.getSimpleName(this) + + ". This may be caused by some unspecified problem in widgetset compilation."); + } + } + + /** + * Returns the renderer associated with this renderer connector. + * <p> + * A subclass of AbstractRendererConnector should override this method as + * shown below. The framework uses + * {@link com.google.gwt.core.client.GWT#create(Class) GWT.create(Class)} to + * create a renderer based on the return type of the overridden method, but + * only if {@link #createRenderer()} is not overridden as well: + * + * <pre> + * public MyRenderer getRenderer() { + * return (MyRenderer) super.getRenderer(); + * } + * </pre> + * + * @return the renderer bound to this connector + */ + public Renderer<T> getRenderer() { + if (renderer == null) { + renderer = createRenderer(); + } + return renderer; + } + + /** + * Creates a new Renderer instance associated with this renderer connector. + * <p> + * You should typically not override this method since the framework by + * default generates an implementation that uses {@link GWT#create(Class)} + * to create a renderer of the same type as returned by the most specific + * override of {@link #getRenderer()}. If you do override the method, you + * can't call <code>super.createRenderer()</code> since the metadata needed + * for that implementation is not generated if there's an override of the + * method. + * + * @return a new renderer to be used with this connector + */ + protected Renderer<T> createRenderer() { + // TODO generate type data + Type type = TypeData.getType(getClass()); + try { + Type rendererType = type.getMethod("getRenderer").getReturnType(); + @SuppressWarnings("unchecked") + Renderer<T> instance = (Renderer<T>) rendererType.createInstance(); + return instance; + } catch (NoDataException e) { + throw new IllegalStateException( + "Default implementation of createRenderer() does not work for " + + Util.getSimpleName(this) + + ". This might be caused by explicitely using " + + "super.createRenderer() or some unspecified " + + "problem with the widgetset compilation.", e); + } + } + + /** + * Decodes the given JSON value into a value of type T so it can be passed + * to the {@link #getRenderer() renderer}. + * + * @param value + * the value to decode + * @return the decoded value of {@code value} + */ + public T decode(JSONValue value) { + @SuppressWarnings("unchecked") + T decodedValue = (T) JsonDecoder.decodeValue(presentationType, value, + null, getConnection()); + return decodedValue; + } + + @Override + @Deprecated + protected void extend(ServerConnector target) { + // NOOP + } + + /** + * Gets the row key for a row index. + * <p> + * In case this renderer wants be able to identify a row in such a way that + * the server also understands it, the row key is used for that. Rows are + * identified by unified keys between the client and the server. + * + * @param index + * the row index for which to get the row key + * @return the row key for the row at {@code index} + */ + protected String getRowKey(int index) { + final ServerConnector parent = getParent(); + if (parent instanceof GridConnector) { + return ((GridConnector) parent).getRowKey(index); + } else { + throw new IllegalStateException("Renderers can only be used " + + "with a Grid."); + } + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java new file mode 100644 index 0000000000..f0c95e2ddf --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import java.util.Collection; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.Style.Visibility; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Base class for renderers that needs initialization and destruction logic + * (override {@link #init(FlyweightCell) and #destroy(FlyweightCell) } and event + * handling (see {@link #onBrowserEvent(Cell, NativeEvent)}, + * {@link #getConsumedEvents()} and {@link #onActivate()}. + * + * <p> + * Also provides a helper method for hiding the cell contents by overriding + * {@link #setContentVisible(FlyweightCell, boolean)} + * + * @since + * @author Vaadin Ltd + */ +public abstract class ComplexRenderer<T> implements Renderer<T> { + + /** + * Called at initialization stage. Perform any initialization here e.g. + * attach handlers, attach widgets etc. + * + * @param cell + * The cell. Note that the cell is not to be stored outside of + * the method as the cell install will change. See + * {@link FlyweightCell} + */ + public void init(FlyweightCell cell) { + // Implement if needed + } + + /** + * Called after the cell is deemed to be destroyed and no longer used by the + * Grid. Called after the cell element is detached from the DOM. + * + * @param cell + * The cell. Note that the cell is not to be stored outside of + * the method as the cell install will change. See + * {@link FlyweightCell} + */ + public void destroy(FlyweightCell cell) { + // Implement if needed + } + + /** + * Returns the events that the renderer should consume. These are also the + * events that the Grid will pass to + * {@link #onBrowserEvent(Cell, NativeEvent)} when they occur. + * <code>null</code> if no events are consumed + * + * @return the consumed events, or null if no events are consumed + * + * @see com.google.gwt.dom.client.BrowserEvents + */ + public Collection<String> getConsumedEvents() { + return null; + } + + /** + * Called whenever a registered event is triggered in the column the + * renderer renders. + * <p> + * The events that triggers this needs to be returned by the + * {@link #getConsumedEvents()} method. + * <p> + * Returns boolean telling if the event has been completely handled and + * should not cause any other actions. + * + * @param cell + * Object containing information about the cell the event was + * triggered on. + * + * @param event + * The original DOM event + * @return true if event should not be handled by grid + */ + public boolean onBrowserEvent(Cell cell, NativeEvent event) { + return false; + } + + /** + * Used by Grid to toggle whether to show actual data or just an empty + * placeholder while data is loading. This method is invoked whenever a cell + * changes between data being available and data missing. + * <p> + * Default implementation hides content by setting visibility: hidden to all + * elements inside the cell. Text nodes are left as is - renderers that add + * such to the root element need to implement explicit support hiding them. + * + * @param cell + * The cell + * @param hasData + * Has the cell content been loaded from the data source + * + */ + public void setContentVisible(FlyweightCell cell, boolean hasData) { + Element cellElement = cell.getElement(); + for (int n = 0; n < cellElement.getChildCount(); n++) { + Node node = cellElement.getChild(n); + if (Element.is(node)) { + Element e = Element.as(node); + if (hasData) { + e.getStyle().clearVisibility(); + } else { + e.getStyle().setVisibility(Visibility.HIDDEN); + } + } + } + } + + /** + * Called when the cell is "activated" by pressing <code>enter</code>, + * double clicking or performing a double tap on the cell. + * + * @return <code>true</code> if event was handled and should not be + * interpreted as a generic gesture by Grid. + */ + public boolean onActivate() { + return false; + } + + /** + * Called when the renderer is deemed to be destroyed and no longer used by + * the Grid. + */ + public void destroy() { + // Implement if needed + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java new file mode 100644 index 0000000000..fc7d3ac833 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat; +import com.google.gwt.i18n.client.TimeZone; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * A renderer for rendering dates into cells + * + * @since + * @author Vaadin Ltd + */ +public class DateRenderer implements Renderer<Date> { + + private DateTimeFormat format = DateTimeFormat + .getFormat(PredefinedFormat.DATE_TIME_SHORT); + + // Calendar is unavailable for GWT + @SuppressWarnings("deprecation") + private TimeZone timeZone = TimeZone.createTimeZone(new Date() + .getTimezoneOffset()); + + @Override + public void render(FlyweightCell cell, Date date) { + String dateStr = format.format(date, timeZone); + cell.getElement().setInnerText(dateStr); + } + + /** + * Gets the format of how the date is formatted. + * + * @return the format + * @see <a + * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT + * documentation on DateTimeFormat</a> + */ + public DateTimeFormat getFormat() { + return format; + } + + /** + * Sets the format used for formatting the dates. + * + * @param format + * the format to set + * @see <a + * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT + * documentation on DateTimeFormat</a> + */ + public void setFormat(DateTimeFormat format) { + if (format == null) { + throw new IllegalArgumentException("Format should not be null"); + } + this.format = format; + } + + /** + * Returns the time zone of the date. + * + * @return the time zone + */ + public TimeZone getTimeZone() { + return timeZone; + } + + /** + * Sets the time zone of the the date. By default uses the time zone of the + * browser. + * + * @param timeZone + * the timeZone to set + */ + public void setTimeZone(TimeZone timeZone) { + if (timeZone == null) { + throw new IllegalArgumentException("Timezone should not be null"); + } + this.timeZone = timeZone; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/DateRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/DateRendererConnector.java new file mode 100644 index 0000000000..52ae7d9b6b --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/DateRendererConnector.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.vaadin.shared.ui.Connect; + +/** + * A connector for {@link com.vaadin.ui.components.grid.renderers.DateRenderer + * DateRenderer}. + * <p> + * The server-side Renderer operates on dates, but the data is serialized as a + * string, and displayed as-is on the client side. This is to be able to support + * the server's locale. + * + * @since + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.renderers.DateRenderer.class) +public class DateRendererConnector extends TextRendererConnector { + // No implementation needed +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java new file mode 100644 index 0000000000..36c5d2bb0f --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Renders a string as HTML into a cell. + * <p> + * The html string is rendered as is without any escaping. It is up to the + * developer to ensure that the html string honors the {@link SafeHtml} + * contract. For more information see + * {@link SafeHtmlUtils#fromSafeConstant(String)}. + * + * @since + * @author Vaadin Ltd + * @see SafeHtmlUtils#fromSafeConstant(String) + */ +public class HtmlRenderer implements Renderer<String> { + + @Override + public void render(FlyweightCell cell, String htmlString) { + cell.getElement().setInnerSafeHtml( + SafeHtmlUtils.fromSafeConstant(htmlString)); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java new file mode 100644 index 0000000000..aa23bc2370 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.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.ui.grid.renderers; + +import com.google.gwt.i18n.client.NumberFormat; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Renders a number into a cell using a specific {@link NumberFormat}. By + * default uses the default number format returned by + * {@link NumberFormat#getDecimalFormat()}. + * + * @since + * @author Vaadin Ltd + * @param <T> + * The number type to render. + */ +public class NumberRenderer<T extends Number> implements Renderer<T> { + + private NumberFormat format = NumberFormat.getDecimalFormat(); + + /** + * Gets the number format that the number should be formatted in. + * + * @return the number format used to render the number + */ + public NumberFormat getFormat() { + return format; + } + + /** + * Sets the number format to use for formatting the number. + * + * @param format + * the format to use + * @throws IllegalArgumentException + * when the format is null + */ + public void setFormat(NumberFormat format) throws IllegalArgumentException { + if (format == null) { + throw new IllegalArgumentException("Format cannot be null"); + } + this.format = format; + } + + @Override + public void render(FlyweightCell cell, Number number) { + cell.getElement().setInnerText(format.format(number)); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/NumberRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/NumberRendererConnector.java new file mode 100644 index 0000000000..cba29d0690 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/NumberRendererConnector.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.vaadin.shared.ui.Connect; + +/** + * A connector for + * {@link com.vaadin.ui.components.grid.renderers.NumberRenderer NumberRenderer} + * . + * <p> + * The server-side Renderer operates on numbers, but the data is serialized as a + * string, and displayed as-is on the client side. This is to be able to support + * the server's locale. + * + * @since + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.renderers.NumberRenderer.class) +public class NumberRendererConnector extends TextRendererConnector { + // no implementation needed +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java new file mode 100644 index 0000000000..d2f3520c43 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Renderer that renders text into a cell. + * + * @since + * @author Vaadin Ltd + */ +public class TextRenderer implements Renderer<String> { + + @Override + public void render(FlyweightCell cell, String text) { + cell.getElement().setInnerText(text); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java new file mode 100644 index 0000000000..9ec609ae06 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.vaadin.shared.ui.Connect; + +/** + * A connector for {@link TextRenderer}. + * + * @since + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.renderers.TextRenderer.class) +public class TextRendererConnector extends AbstractRendererConnector<String> { + + @Override + public TextRenderer getRenderer() { + return (TextRenderer) super.getRenderer(); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java new file mode 100644 index 0000000000..1d4a8c0384 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; +import com.vaadin.shared.ui.Connect; + +/** + * A connector for {@link UnsafeHtmlRenderer} + * + * @since + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.renderers.HtmlRenderer.class) +public class UnsafeHtmlRendererConnector extends + AbstractRendererConnector<String> { + + public static class UnsafeHtmlRenderer implements Renderer<String> { + @Override + public void render(FlyweightCell cell, String data) { + cell.getElement().setInnerHTML(data); + } + } + + @Override + public UnsafeHtmlRenderer getRenderer() { + return (UnsafeHtmlRenderer) super.getRenderer(); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java new file mode 100644 index 0000000000..b7cd72600a --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.renderers; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.Util; +import com.vaadin.client.ui.grid.FlyweightCell; + +/** + * A renderer for rendering widgets into cells. + * + * @since + * @author Vaadin Ltd + * @param <T> + * the row data type + * @param <W> + * the Widget type + */ +public abstract class WidgetRenderer<T, W extends Widget> extends + ComplexRenderer<T> { + + /** + * Creates a widget to attach to a cell. The widgets will be attached to the + * cell after the cell element has been attached to DOM. + * + * @return widget to attach to a cell. All returned instances should be new + * widget instances without a parent. + */ + public abstract W createWidget(); + + @Override + public void render(FlyweightCell cell, T data) { + W w = Util.findWidget(cell.getElement().getFirstChildElement(), null); + assert w != null : "Widget not found in cell (" + cell.getColumn() + + "," + cell.getRow() + ")"; + render(cell, data, w); + } + + /** + * Renders a cell with a widget. This provides a way to update any + * information in the widget that is cell specific. Do not detach the Widget + * here, it will be done automatically by the Grid when the widget is no + * longer needed. + * + * @param cell + * the cell to render + * @param data + * the data of the cell + * @param widget + * the widget embedded in the cell + */ + public abstract void render(FlyweightCell cell, T data, W widget); + +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java b/client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java new file mode 100644 index 0000000000..f55229d86c --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import com.vaadin.client.data.DataSource.RowHandle; + +/** + * An abstract class that adds a consistent API for common methods that's needed + * by Vaadin's server-based selection models to work. + * <p> + * <em>Note:</em> This should be an interface instead of an abstract class, if + * only we could define protected methods in an interface. + * + * @author Vaadin Ltd + * @param <T> + * The grid's row type + */ +public abstract class AbstractRowHandleSelectionModel<T> implements + SelectionModel<T> { + /** + * Select a row, based on its + * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}. + * <p> + * <em>Note:</em> this method may not fire selection change events. + * + * @param handle + * the handle to select by + * @return <code>true</code> iff the selection state was changed by this + * call + * @throws UnsupportedOperationException + * if the selection model does not support either handles or + * selection + */ + protected abstract boolean selectByHandle(RowHandle<T> handle); + + /** + * Deselect a row, based on its + * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}. + * <p> + * <em>Note:</em> this method may not fire selection change events. + * + * @param handle + * the handle to deselect by + * @return <code>true</code> iff the selection state was changed by this + * call + * @throws UnsupportedOperationException + * if the selection model does not support either handles or + * deselection + */ + protected abstract boolean deselectByHandle(RowHandle<T> handle) + throws UnsupportedOperationException; +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java b/client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java new file mode 100644 index 0000000000..342c426b55 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import com.google.gwt.event.shared.HandlerRegistration; + +/** + * Marker interface for widgets that fires selection change events. + * + * @author Vaadin Ltd + * @since + */ +public interface HasSelectionChangeHandlers<T> { + + /** + * Register a selection change handler. + * <p> + * This handler is called whenever a + * {@link com.vaadin.ui.components.grid.selection.SelectionModel + * SelectionModel} detects a change in selection state. + * + * @param handler + * a {@link SelectionChangeHandler} + * @return a handler registration object, which can be used to remove the + * handler. + */ + public HandlerRegistration addSelectionChangeHandler( + SelectionChangeHandler<T> handler); + +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java new file mode 100644 index 0000000000..a3030e3a1f --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java @@ -0,0 +1,764 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import com.google.gwt.animation.client.AnimationScheduler; +import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; +import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.InputElement; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.TableElement; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.vaadin.client.Util; +import com.vaadin.client.data.AbstractRemoteDataSource; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.DataAvailableEvent; +import com.vaadin.client.ui.grid.DataAvailableHandler; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.keyevents.BodyKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.BodyKeyUpHandler; +import com.vaadin.client.ui.grid.keyevents.GridKeyDownEvent; +import com.vaadin.client.ui.grid.keyevents.GridKeyUpEvent; +import com.vaadin.client.ui.grid.renderers.ComplexRenderer; +import com.vaadin.client.ui.grid.selection.SelectionModel.Multi.Batched; +import com.vaadin.shared.ui.grid.ScrollDestination; + +/* This class will probably not survive the final merge of all selection functionality. */ +public class MultiSelectionRenderer<T> extends ComplexRenderer<Boolean> { + + /** The size of the autoscroll area, both top and bottom. */ + private static final int SCROLL_AREA_GRADIENT_PX = 100; + + /** The maximum number of pixels per second to autoscroll. */ + private static final int SCROLL_TOP_SPEED_PX_SEC = 500; + + /** + * The minimum area where the grid doesn't scroll while the pointer is + * pressed. + */ + private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50; + + /** + * This class's main objective is to listen when to stop autoscrolling, and + * make sure everything stops accordingly. + */ + private class TouchEventHandler implements NativePreviewHandler { + @Override + public void onPreviewNativeEvent(final NativePreviewEvent event) { + switch (event.getTypeInt()) { + case Event.ONTOUCHSTART: { + if (event.getNativeEvent().getTouches().length() == 1) { + /* + * Something has dropped a touchend/touchcancel and the + * scroller is most probably running amok. Let's cancel it + * and pretend that everything's going as expected + * + * Because this is a preview, this code is run before the + * event handler in MultiSelectionRenderer.onBrowserEvent. + * Therefore, we can simply kill everything and let that + * method restart things as they should. + */ + autoScrollHandler.stop(); + + /* + * Related TODO: investigate why iOS seems to ignore a + * touchend/touchcancel when frames are dropped, and/or if + * something can be done about that. + */ + } + break; + } + + case Event.ONTOUCHMOVE: + event.cancel(); + break; + + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + /* + * Remember: targetElement is always where touchstart started, + * not where the finger is pointing currently. + */ + final Element targetElement = Element.as(event.getNativeEvent() + .getEventTarget()); + if (isInFirstColumn(targetElement)) { + removeNativeHandler(); + event.cancel(); + } + break; + } + } + + private boolean isInFirstColumn(final Element element) { + if (element == null) { + return false; + } + final Element tbody = getTbodyElement(); + + if (tbody == null || !tbody.isOrHasChild(element)) { + return false; + } + + /* + * The null-parent in the while clause is in the case where element + * is an immediate tr child in the tbody. Should never happen in + * internal code, but hey... + */ + Element cursor = element; + while (cursor.getParentElement() != null + && cursor.getParentElement().getParentElement() != tbody) { + cursor = cursor.getParentElement(); + } + + final Element tr = cursor.getParentElement(); + return tr.getFirstChildElement().equals(cursor); + } + } + + /** + * This class's responsibility is to + * <ul> + * <li>scroll the table while a pointer is kept in a scrolling zone and + * <li>select rows whenever a pointer is "activated" on a selection cell + * </ul> + * <p> + * <em>Techical note:</em> This class is an AnimationCallback because we + * need a timer: when the finger is kept in place while the grid scrolls, we + * still need to be able to make new selections. So, instead of relying on + * events (which won't be fired, since the pointer isn't necessarily + * moving), we do this check on each frame while the pointer is "active" + * (mouse is pressed, finger is on screen). + */ + private class AutoScrollerAndSelector implements AnimationCallback { + + /** + * If the acceleration gradient area is smaller than this, autoscrolling + * will be disabled (it becomes too quick to accelerate to be usable). + */ + private static final int GRADIENT_MIN_THRESHOLD_PX = 10; + + /** + * The speed at which the gradient area recovers, once scrolling in that + * direction has started. + */ + private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1; + private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC / 1000.0d; + + /** + * The lowest y-coordinate on the {@link Event#getClientY() client} from + * where we need to start scrolling towards the top. + */ + private int topBound = -1; + + /** + * The highest y-coordinate on the {@link Event#getClientY() client} + * from where we need to scrolling towards the bottom. + */ + private int bottomBound = -1; + + /** + * <code>true</code> if the pointer is selecting, <code>false</code> if + * the pointer is deselecting. + */ + private final boolean selectionPaint; + + /** + * The area where the selection acceleration takes place. If < + * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled + */ + private final int gradientArea; + + /** + * The number of pixels per seconds we currently are scrolling (negative + * is towards the top, positive is towards the bottom). + */ + private double scrollSpeed = 0; + + private double prevTimestamp = 0; + + /** + * This field stores fractions of pixels to scroll, to make sure that + * we're able to scroll less than one px per frame. + */ + private double pixelsToScroll = 0.0d; + + /** Should this animator be running. */ + private boolean running = false; + + /** The handle in which this instance is running. */ + private AnimationHandle handle; + + /** The pointer's pageX coordinate. */ + private int pageX; + + /** The pointer's pageY coordinate. */ + private int pageY; + + /** The logical index of the row that was most recently modified. */ + private int logicalRow = -1; + + /** @see #doScrollAreaChecks(int) */ + private int finalTopBound; + + /** @see #doScrollAreaChecks(int) */ + private int finalBottomBound; + + private boolean scrollAreaShouldRebound = false; + + private AbstractRemoteDataSource<T> remoteDataSource = null; + private Batched<T> batchedSelectionModel = null; + + public AutoScrollerAndSelector(final int topBound, + final int bottomBound, final int gradientArea, + final boolean selectionPaint) { + finalTopBound = topBound; + finalBottomBound = bottomBound; + this.gradientArea = gradientArea; + this.selectionPaint = selectionPaint; + } + + @Override + public void execute(final double timestamp) { + final double timeDiff = timestamp - prevTimestamp; + prevTimestamp = timestamp; + + reboundScrollArea(timeDiff); + + pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d); + final int intPixelsToScroll = (int) pixelsToScroll; + pixelsToScroll -= intPixelsToScroll; + + if (intPixelsToScroll != 0) { + grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll); + } + + @SuppressWarnings("hiding") + int logicalRow = getLogicalRowIndex(Util.getElementFromPoint(pageX, + pageY)); + if (logicalRow != -1 && logicalRow != this.logicalRow) { + this.logicalRow = logicalRow; + setSelected(logicalRow, selectionPaint); + + if (remoteDataSource != null && batchedSelectionModel != null) { + Collection<T> pinneds = batchedSelectionModel + .getSelectedRowsBatch(); + pinneds.addAll(batchedSelectionModel + .getDeselectedRowsBatch()); + remoteDataSource.transactionPin(pinneds); + } + } + + reschedule(); + } + + /** + * If the scroll are has been offset by the pointer starting out there, + * move it back a bit + */ + private void reboundScrollArea(double timeDiff) { + if (!scrollAreaShouldRebound) { + return; + } + + int reboundPx = (int) Math.ceil(SCROLL_AREA_REBOUND_PX_PER_MS + * timeDiff); + if (topBound < finalTopBound) { + topBound += reboundPx; + topBound = Math.min(topBound, finalTopBound); + updateScrollSpeed(pageY); + } else if (bottomBound > finalBottomBound) { + bottomBound -= reboundPx; + bottomBound = Math.max(bottomBound, finalBottomBound); + updateScrollSpeed(pageY); + } + } + + private void updateScrollSpeed(final int pointerPageY) { + + final double ratio; + if (pointerPageY < topBound) { + final double distance = pointerPageY - topBound; + ratio = Math.max(-1, distance / gradientArea); + } + + else if (pointerPageY > bottomBound) { + final double distance = pointerPageY - bottomBound; + ratio = Math.min(1, distance / gradientArea); + } + + else { + ratio = 0; + } + + scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC; + } + + @SuppressWarnings("deprecation") + public void start(int logicalRowIndex) { + running = true; + setSelected(logicalRowIndex, selectionPaint); + logicalRow = logicalRowIndex; + reschedule(); + + DataSource<T> dataSource = grid.getDataSource(); + SelectionModel<T> selectionModel = grid.getSelectionModel(); + if (dataSource instanceof AbstractRemoteDataSource + && selectionModel instanceof Batched) { + this.remoteDataSource = (AbstractRemoteDataSource<T>) dataSource; + this.batchedSelectionModel = (Batched<T>) selectionModel; + + Collection<T> pinneds = batchedSelectionModel + .getSelectedRowsBatch(); + pinneds.addAll(batchedSelectionModel.getDeselectedRowsBatch()); + remoteDataSource.transactionPin(pinneds); + } + } + + @SuppressWarnings("deprecation") + public void stop() { + running = false; + + // split into two lines because of Java generics not playing nice. + @SuppressWarnings("unchecked") + Collection<T> emptySet = (Collection<T>) Collections.emptySet(); + remoteDataSource.transactionPin(emptySet); + remoteDataSource = null; + batchedSelectionModel = null; + + if (handle != null) { + handle.cancel(); + handle = null; + } + } + + private void reschedule() { + if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) { + handle = AnimationScheduler.get().requestAnimationFrame(this, + grid.getElement()); + } + } + + @SuppressWarnings("hiding") + public void updatePointerCoords(int pageX, int pageY) { + doScrollAreaChecks(pageY); + updateScrollSpeed(pageY); + this.pageX = pageX; + this.pageY = pageY; + } + + /** + * This method checks whether the first pointer event started in an area + * that would start scrolling immediately, and does some actions + * accordingly. + * <p> + * If it is, that scroll area will be offset "beyond" the pointer (above + * if pointer is towards the top, otherwise below). + * <p> + * <span style="font-size:smaller">*) This behavior will change in + * future patches (henrik paul 2.7.2014)</span> + */ + private void doScrollAreaChecks(int pageY) { + /* + * The first run makes sure that neither scroll position is + * underneath the finger, but offset to either direction from + * underneath the pointer. + */ + if (topBound == -1) { + topBound = Math.min(finalTopBound, pageY); + bottomBound = Math.max(finalBottomBound, pageY); + } + + /* + * Subsequent runs make sure that the scroll area grows (but doesn't + * shrink) with the finger, but no further than the final bound. + */ + else { + int oldTopBound = topBound; + if (topBound < finalTopBound) { + topBound = Math.max(topBound, + Math.min(finalTopBound, pageY)); + } + + int oldBottomBound = bottomBound; + if (bottomBound > finalBottomBound) { + bottomBound = Math.min(bottomBound, + Math.max(finalBottomBound, pageY)); + } + + final boolean topDidNotMove = oldTopBound == topBound; + final boolean bottomDidNotMove = oldBottomBound == bottomBound; + final boolean wasVerticalMovement = pageY != this.pageY; + scrollAreaShouldRebound = (topDidNotMove && bottomDidNotMove && wasVerticalMovement); + } + } + } + + /** + * This class makes sure that pointer movemenets are registered and + * delegated to the autoscroller so that it can: + * <ul> + * <li>modify the speed in which we autoscroll. + * <li>"paint" a new row with the selection. + * </ul> + * Essentially, when a pointer is pressed on the selection column, a native + * preview handler is registered (so that selection gestures can happen + * outside of the selection column). The handler itself makes sure that it's + * detached when the pointer is "lifted". + */ + private class AutoScrollHandler { + private AutoScrollerAndSelector autoScroller; + + /** The registration info for {@link #scrollPreviewHandler} */ + private HandlerRegistration handlerRegistration; + + private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() { + @Override + public void onPreviewNativeEvent(final NativePreviewEvent event) { + if (autoScroller == null) { + stop(); + return; + } + + final NativeEvent nativeEvent = event.getNativeEvent(); + int pageY = 0; + int pageX = 0; + switch (event.getTypeInt()) { + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + pageY = Util.getTouchOrMouseClientY(nativeEvent); + pageX = Util.getTouchOrMouseClientX(nativeEvent); + autoScroller.updatePointerCoords(pageX, pageY); + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + stop(); + break; + } + } + }; + + /** + * The top bound, as calculated from the {@link Event#getClientY() + * client} coordinates. + */ + private int topBound = -1; + + /** + * The bottom bound, as calculated from the {@link Event#getClientY() + * client} coordinates. + */ + private int bottomBound = -1; + + /** The size of the autoscroll acceleration area. */ + private int gradientArea; + + public void start(int logicalRowIndex) { + + SelectionModel<T> model = grid.getSelectionModel(); + if (model instanceof Batched) { + Batched<?> batchedModel = (Batched<?>) model; + batchedModel.startBatchSelect(); + } + + /* + * bounds are updated whenever the autoscroll cycle starts, to make + * sure that the widget hasn't changed in size, moved around, or + * whatnot. + */ + updateScrollBounds(); + + assert handlerRegistration == null : "handlerRegistration was not null"; + assert autoScroller == null : "autoScroller was not null"; + handlerRegistration = Event + .addNativePreviewHandler(scrollPreviewHandler); + + autoScroller = new AutoScrollerAndSelector(topBound, bottomBound, + gradientArea, !isSelected(logicalRowIndex)); + autoScroller.start(logicalRowIndex); + } + + private void updateScrollBounds() { + final Element root = Element.as(grid.getElement()); + final Element tableWrapper = Element.as(root.getChild(2)); + final TableElement table = TableElement.as(tableWrapper + .getFirstChildElement()); + final Element thead = table.getTHead(); + final Element tfoot = table.getTFoot(); + + /* + * GWT _does_ have an "Element.getAbsoluteTop()" that takes both the + * client top and scroll compensation into account, but they're + * calculated wrong for our purposes, so this does something + * similar, but only suitable for us. + * + * Also, this should be a bit faster, since the scroll compensation + * is calculated only once and used in two places. + */ + + final int topBorder = getClientTop(root) + thead.getOffsetHeight(); + final int bottomBorder = getClientTop(tfoot); + + final int scrollCompensation = getScrollCompensation(); + topBound = scrollCompensation + topBorder + SCROLL_AREA_GRADIENT_PX; + bottomBound = scrollCompensation + bottomBorder + - SCROLL_AREA_GRADIENT_PX; + gradientArea = SCROLL_AREA_GRADIENT_PX; + + // modify bounds if they're too tightly packed + if (bottomBound - topBound < MIN_NO_AUTOSCROLL_AREA_PX) { + int adjustment = MIN_NO_AUTOSCROLL_AREA_PX + - (bottomBound - topBound); + topBound -= adjustment / 2; + bottomBound += adjustment / 2; + gradientArea -= adjustment / 2; + } + } + + /** Get the "top" of an element in relation to "client" coordinates. */ + private int getClientTop(final Element e) { + Element cursor = e; + int top = 0; + while (cursor != null) { + top += cursor.getOffsetTop(); + cursor = cursor.getOffsetParent(); + } + return top; + } + + private int getScrollCompensation() { + Element cursor = grid.getElement(); + int scroll = 0; + while (cursor != null) { + scroll -= cursor.getScrollTop(); + cursor = cursor.getParentElement(); + } + + return scroll; + } + + public void stop() { + if (handlerRegistration != null) { + handlerRegistration.removeHandler(); + handlerRegistration = null; + } + + if (autoScroller != null) { + autoScroller.stop(); + autoScroller = null; + } + + SelectionModel<T> model = grid.getSelectionModel(); + if (model instanceof Batched) { + Batched<?> batchedModel = (Batched<?>) model; + batchedModel.commitBatchSelect(); + } + + removeNativeHandler(); + } + } + + private class SpaceKeyDownSelectHandler implements BodyKeyDownHandler<T> { + + private HandlerRegistration scrollHandler = null; + private boolean spaceDown = false; + + @Override + public void onKeyDown(GridKeyDownEvent<T> event) { + if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE || spaceDown) { + return; + } + + spaceDown = true; + Cell active = event.getActiveCell(); + final int rowIndex = active.getRow(); + + if (scrollHandler != null) { + scrollHandler.removeHandler(); + scrollHandler = null; + } + + scrollHandler = grid + .addDataAvailableHandler(new DataAvailableHandler() { + + @Override + public void onDataAvailable(DataAvailableEvent event) { + if (event.getAvailableRows().contains(rowIndex)) { + setSelected(rowIndex, !isSelected(rowIndex)); + scrollHandler.removeHandler(); + scrollHandler = null; + } + } + }); + grid.scrollToRow(rowIndex, ScrollDestination.ANY); + } + + } + + private static final String LOGICAL_ROW_PROPERTY_INT = "vEscalatorLogicalRow"; + + private final Grid<T> grid; + private HandlerRegistration nativePreviewHandlerRegistration; + private final SpaceKeyDownSelectHandler handler = new SpaceKeyDownSelectHandler(); + private HandlerRegistration spaceDown; + private HandlerRegistration spaceUp; + + private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler(); + + public MultiSelectionRenderer(final Grid<T> grid) { + this.grid = grid; + spaceDown = grid.addKeyDownHandler(handler); + spaceUp = grid.addKeyUpHandler(new BodyKeyUpHandler<T>() { + + @Override + public void onKeyUp(GridKeyUpEvent<T> event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_SPACE) { + handler.spaceDown = false; + } + } + }); + } + + @Override + public void destroy() { + spaceDown.removeHandler(); + spaceUp.removeHandler(); + if (nativePreviewHandlerRegistration != null) { + removeNativeHandler(); + } + } + + @Override + public void render(final FlyweightCell cell, final Boolean data) { + /* + * FIXME: Once https://dev.vaadin.com/review/#/c/3670/ is merged + * (init/destroy), split this method. Also, remove all event preview + * handlers on detach, to avoid hanging events. + */ + + final InputElement checkbox = InputElement.as(DOM.createInputCheck()); + checkbox.setChecked(data.booleanValue()); + checkbox.setPropertyInt(LOGICAL_ROW_PROPERTY_INT, cell.getRow()); + cell.getElement().removeAllChildren(); + cell.getElement().appendChild(checkbox); + } + + @Override + public Collection<String> getConsumedEvents() { + final HashSet<String> events = new HashSet<String>(); + + /* + * this column's first interest is only to attach a NativePreventHandler + * that does all the magic. These events are the beginning of that + * cycle. + */ + events.add(BrowserEvents.MOUSEDOWN); + events.add(BrowserEvents.TOUCHSTART); + + return events; + } + + @Override + public boolean onBrowserEvent(final Cell cell, final NativeEvent event) { + if (BrowserEvents.TOUCHSTART.equals(event.getType()) + || BrowserEvents.MOUSEDOWN.equals(event.getType())) { + injectNativeHandler(); + int logicalRowIndex = getLogicalRowIndex(Element.as(event + .getEventTarget())); + autoScrollHandler.start(logicalRowIndex); + event.preventDefault(); + event.stopPropagation(); + return true; + } else { + throw new IllegalStateException("received unexpected event: " + + event.getType()); + } + } + + private void injectNativeHandler() { + removeNativeHandler(); + nativePreviewHandlerRegistration = Event + .addNativePreviewHandler(new TouchEventHandler()); + } + + private void removeNativeHandler() { + if (nativePreviewHandlerRegistration != null) { + nativePreviewHandlerRegistration.removeHandler(); + nativePreviewHandlerRegistration = null; + } + } + + private int getLogicalRowIndex(final Element target) { + /* + * We can't simply go backwards until we find a <tr> first element, + * because of the table-in-table scenario. We need to, unfortunately, go + * up from our known root. + */ + final Element tbody = getTbodyElement(); + Element tr = tbody.getFirstChildElement(); + while (tr != null) { + if (tr.isOrHasChild(target)) { + final Element td = tr.getFirstChildElement(); + assert td != null : "Cell has disappeared"; + + final Element checkbox = td.getFirstChildElement(); + assert checkbox != null : "Checkbox has disappeared"; + + return checkbox.getPropertyInt(LOGICAL_ROW_PROPERTY_INT); + } + tr = tr.getNextSiblingElement(); + } + return -1; + } + + private Element getTbodyElement() { + final Element root = grid.getElement(); + final Element tablewrapper = Element.as(root.getChild(2)); + if (tablewrapper != null) { + final TableElement table = TableElement.as(tablewrapper + .getFirstChildElement()); + return table.getTBodies().getItem(0); + } else { + return null; + } + } + + protected boolean isSelected(final int logicalRow) { + return grid.isSelected(grid.getDataSource().getRow(logicalRow)); + } + + protected void setSelected(final int logicalRow, final boolean select) { + T row = grid.getDataSource().getRow(logicalRow); + if (select) { + grid.select(row); + } else { + grid.deselect(row); + } + } +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java new file mode 100644 index 0000000000..4bebaf0fbb --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java @@ -0,0 +1,170 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.client.ui.grid.Grid; + +/** + * Event object describing a change in Grid row selection state. + * + * @since + * @author Vaadin Ltd + */ +@SuppressWarnings("rawtypes") +public class SelectionChangeEvent<T> extends GwtEvent<SelectionChangeHandler> { + + private static final Type<SelectionChangeHandler> eventType = new Type<SelectionChangeHandler>(); + + private final Grid<T> grid; + private final List<T> added; + private final List<T> removed; + private final boolean batched; + + /** + * Creates an event with a single added or removed row. + * + * @param grid + * grid reference, used for getSource + * @param added + * the added row, or <code>null</code> if a row was not added + * @param removed + * the removed row, or <code>null</code> if a row was not removed + * @param batched + * whether or not this selection change event is triggered during + * a batched selection/deselection action + * @see SelectionModel.Multi.Batched + */ + public SelectionChangeEvent(Grid<T> grid, T added, T removed, + boolean batched) { + this.grid = grid; + this.batched = batched; + + if (added != null) { + this.added = Collections.singletonList(added); + } else { + this.added = Collections.emptyList(); + } + + if (removed != null) { + this.removed = Collections.singletonList(removed); + } else { + this.removed = Collections.emptyList(); + } + } + + /** + * Creates an event where several rows have been added or removed. + * + * @param grid + * Grid reference, used for getSource + * @param added + * a collection of added rows, or <code>null</code> if no rows + * were added + * @param removed + * a collection of removed rows, or <code>null</code> if no rows + * were removed + * @param batched + * whether or not this selection change event is triggered during + * a batched selection/deselection action + * @see SelectionModel.Multi.Batched + */ + public SelectionChangeEvent(Grid<T> grid, Collection<T> added, + Collection<T> removed, boolean batched) { + this.grid = grid; + this.batched = batched; + + if (added != null) { + this.added = new ArrayList<T>(added); + } else { + this.added = Collections.emptyList(); + } + + if (removed != null) { + this.removed = new ArrayList<T>(removed); + } else { + this.removed = Collections.emptyList(); + } + } + + /** + * Get a reference to the Grid object that fired this event. + * + * @return a grid reference + */ + @Override + public Grid<T> getSource() { + return grid; + } + + /** + * Get all rows added to the selection since the last + * {@link SelectionChangeEvent}. + * + * @return a collection of added rows. Empty collection if no rows were + * added. + */ + public Collection<T> getAdded() { + return Collections.unmodifiableCollection(added); + } + + /** + * Get all rows removed from the selection since the last + * {@link SelectionChangeEvent}. + * + * @return a collection of removed rows. Empty collection if no rows were + * removed. + */ + public Collection<T> getRemoved() { + return Collections.unmodifiableCollection(removed); + } + + /** + * Gets a type identifier for this event. + * + * @return a {@link Type} identifier. + */ + public static Type<SelectionChangeHandler> getType() { + return eventType; + } + + @Override + public Type<SelectionChangeHandler> getAssociatedType() { + return eventType; + } + + @Override + @SuppressWarnings("unchecked") + protected void dispatch(SelectionChangeHandler handler) { + handler.onSelectionChange(this); + } + + /** + * Checks if this selection change event is fired during a batched + * selection/deselection operation. + * + * @return <code>true</code> iff this event is fired during a batched + * selection/deselection operation + */ + public boolean isBatchedSelection() { + return batched; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.java new file mode 100644 index 0000000000..a469f5af1f --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.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.ui.grid.selection; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for {@link SelectionChangeEvent}s. + * + * @since + * @author Vaadin Ltd + * @param <T> + * The row data type + */ +public interface SelectionChangeHandler<T> extends EventHandler { + + /** + * Called when a selection model's selection state is changed. + * + * @param event + * a selection change event, containing info about rows that have + * been added to or removed from the selection. + */ + public void onSelectionChange(SelectionChangeEvent<T> event); + +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java new file mode 100644 index 0000000000..55336ce3da --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java @@ -0,0 +1,237 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import java.util.Collection; + +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Common interface for all selection models. + * <p> + * Selection models perform tracking of selected rows in the Grid, as well as + * dispatching events when the selection state changes. + * + * @author Vaadin Ltd + * @since + * @param <T> + * Grid's row type + */ +public interface SelectionModel<T> { + + /** + * Return true if the provided row is considered selected under the + * implementing selection model. + * + * @param row + * row object instance + * @return <code>true</code>, if the row given as argument is considered + * selected. + */ + public boolean isSelected(T row); + + /** + * Return the {@link Renderer} responsible for rendering the selection + * column. + * + * @return a renderer instance. If null is returned, a selection column will + * not be drawn. + */ + public Renderer<Boolean> getSelectionColumnRenderer(); + + /** + * Tells this SelectionModel which Grid it belongs to. + * <p> + * Implementations are free to have this be a no-op. This method is called + * internally by Grid. + * + * @param grid + * a {@link Grid} instance + */ + public void setGrid(Grid<T> grid); + + /** + * Resets the SelectionModel to the initial state. + * <p> + * This method can be called internally, for example, when the attached + * Grid's data source changes. + */ + public void reset(); + + /** + * Returns a Collection containing all selected rows. + * + * @return a non-null collection. + */ + public Collection<T> getSelectedRows(); + + /** + * Selection model that allows a maximum of one row to be selected at any + * one time. + * + * @param <T> + * type parameter corresponding with Grid row type + */ + public interface Single<T> extends SelectionModel<T> { + + /** + * Selects a row. + * + * @param row + * a {@link Grid} row object + * @return true, if this row as not previously selected. + */ + public boolean select(T row); + + /** + * Deselects a row. + * <p> + * This is a no-op unless {@link row} is the currently selected row. + * + * @param row + * a {@link Grid} row object + * @return true, if the currently selected row was deselected. + */ + public boolean deselect(T row); + + /** + * Returns the currently selected row. + * + * @return a {@link Grid} row object or null, if nothing is selected. + */ + public T getSelectedRow(); + + } + + /** + * Selection model that allows for several rows to be selected at once. + * + * @param <T> + * type parameter corresponding with Grid row type + */ + public interface Multi<T> extends SelectionModel<T> { + + /** + * A multi selection model that can send selections and deselections in + * a batch, instead of committing them one-by-one. + * + * @param <T> + * type parameter corresponding with Grid row type + */ + public interface Batched<T> extends Multi<T> { + /** + * Starts a batch selection. + * <p> + * Any commands to any select or deselect method will be batched + * into one, and a final selection event will be fired when + * {@link #commitBatchSelect()} is called. + * <p> + * <em>Note:</em> {@link SelectionChangeEvent SelectionChangeEvents} + * will still be fired for each selection/deselection. You should + * check whether the event is a part of a batch or not with + * {@link SelectionChangeEvent#isBatchedSelection()}. + */ + public void startBatchSelect(); + + /** + * Commits and ends a batch selection. + * <p> + * Any and all selections and deselections since the last invocation + * of {@link #startBatchSelect()} will be fired at once as one + * collated {@link SelectionChangeEvent}. + */ + public void commitBatchSelect(); + + /** + * Checks whether or not a batch has been started. + * + * @return <code>true</code> iff a batch has been started + */ + public boolean isBeingBatchSelected(); + + /** + * Gets all the rows that would become selected in this batch. + * + * @return a collection of the rows that would become selected + */ + public Collection<T> getSelectedRowsBatch(); + + /** + * Gets all the rows that would become deselected in this batch. + * + * @return a collection of the rows that would become deselected + */ + public Collection<T> getDeselectedRowsBatch(); + } + + /** + * Selects one or more rows. + * + * @param rows + * {@link Grid} row objects + * @return true, if the set of selected rows was changed. + */ + public boolean select(T... rows); + + /** + * Deselects one or more rows. + * + * @param rows + * Grid row objects + * @return true, if the set of selected rows was changed. + */ + public boolean deselect(T... rows); + + /** + * De-selects all rows. + * + * @return true, if any row was previously selected. + */ + public boolean deselectAll(); + + /** + * Select all rows in a {@link Collection}. + * + * @param rows + * a collection of Grid row objects + * @return true, if the set of selected rows was changed. + */ + public boolean select(Collection<T> rows); + + /** + * Deselect all rows in a {@link Collection}. + * + * @param rows + * a collection of Grid row objects + * @return true, if the set of selected rows was changed. + */ + public boolean deselect(Collection<T> rows); + + } + + /** + * Interface for a selection model that does not allow anything to be + * selected. + * + * @param <T> + * type parameter corresponding with Grid row type + */ + public interface None<T> extends SelectionModel<T> { + + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java new file mode 100644 index 0000000000..b6ecc945e2 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java @@ -0,0 +1,258 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Multi-row selection model. + * + * @author Vaadin Ltd + * @since + */ +public class SelectionModelMulti<T> extends AbstractRowHandleSelectionModel<T> + implements SelectionModel.Multi.Batched<T> { + + private final LinkedHashSet<RowHandle<T>> selectedRows; + private Renderer<Boolean> renderer; + private Grid<T> grid; + + private boolean batchStarted = false; + private final LinkedHashSet<RowHandle<T>> selectionBatch = new LinkedHashSet<RowHandle<T>>(); + private final LinkedHashSet<RowHandle<T>> deselectionBatch = new LinkedHashSet<RowHandle<T>>(); + + public SelectionModelMulti() { + grid = null; + renderer = null; + selectedRows = new LinkedHashSet<RowHandle<T>>(); + } + + @Override + public boolean isSelected(T row) { + return isSelectedByHandle(grid.getDataSource().getHandle(row)); + } + + @Override + public Renderer<Boolean> getSelectionColumnRenderer() { + return renderer; + } + + @Override + public void setGrid(Grid<T> grid) { + if (grid == null) { + throw new IllegalArgumentException("Grid cannot be null"); + } + + if (this.grid == null) { + this.grid = grid; + } else { + throw new IllegalStateException( + "Grid reference cannot be reassigned"); + } + + this.renderer = new MultiSelectionRenderer<T>(grid); + } + + @Override + public boolean select(T... rows) { + if (rows == null) { + throw new IllegalArgumentException("Rows cannot be null"); + } + return select(Arrays.asList(rows)); + } + + @Override + public boolean deselect(T... rows) { + if (rows == null) { + throw new IllegalArgumentException("Rows cannot be null"); + } + return deselect(Arrays.asList(rows)); + } + + @Override + public boolean deselectAll() { + if (selectedRows.size() > 0) { + + @SuppressWarnings("unchecked") + final LinkedHashSet<RowHandle<T>> selectedRowsClone = (LinkedHashSet<RowHandle<T>>) selectedRows + .clone(); + SelectionChangeEvent<T> event = new SelectionChangeEvent<T>(grid, + null, getSelectedRows(), isBeingBatchSelected()); + selectedRows.clear(); + + if (isBeingBatchSelected()) { + selectionBatch.clear(); + deselectionBatch.clear(); + deselectionBatch.addAll(selectedRowsClone); + } + + grid.fireEvent(event); + return true; + } + return false; + } + + @Override + public boolean select(Collection<T> rows) { + if (rows == null) { + throw new IllegalArgumentException("Rows cannot be null"); + } + + Set<T> added = new LinkedHashSet<T>(); + + for (T row : rows) { + RowHandle<T> handle = grid.getDataSource().getHandle(row); + if (selectByHandle(handle)) { + added.add(row); + } + } + + if (added.size() > 0) { + grid.fireEvent(new SelectionChangeEvent<T>(grid, added, null, + isBeingBatchSelected())); + + return true; + } + return false; + } + + @Override + public boolean deselect(Collection<T> rows) { + if (rows == null) { + throw new IllegalArgumentException("Rows cannot be null"); + } + + Set<T> removed = new LinkedHashSet<T>(); + + for (T row : rows) { + RowHandle<T> handle = grid.getDataSource().getHandle(row); + if (deselectByHandle(handle)) { + removed.add(row); + } + } + + if (removed.size() > 0) { + grid.fireEvent(new SelectionChangeEvent<T>(grid, null, removed, + isBeingBatchSelected())); + return true; + } + return false; + } + + protected boolean isSelectedByHandle(RowHandle<T> handle) { + return selectedRows.contains(handle); + } + + @Override + protected boolean selectByHandle(RowHandle<T> handle) { + if (selectedRows.add(handle)) { + handle.pin(); + + if (isBeingBatchSelected()) { + deselectionBatch.remove(handle); + selectionBatch.add(handle); + } + + return true; + } + return false; + } + + @Override + protected boolean deselectByHandle(RowHandle<T> handle) { + if (selectedRows.remove(handle)) { + if (!isBeingBatchSelected()) { + handle.unpin(); + } else { + selectionBatch.remove(handle); + deselectionBatch.add(handle); + } + return true; + } + return false; + } + + @Override + public Collection<T> getSelectedRows() { + Set<T> selected = new LinkedHashSet<T>(); + for (RowHandle<T> handle : selectedRows) { + selected.add(handle.getRow()); + } + return Collections.unmodifiableSet(selected); + } + + @Override + public void reset() { + deselectAll(); + } + + @Override + public void startBatchSelect() { + assert !isBeingBatchSelected() : "Batch has already been started"; + batchStarted = true; + } + + @Override + public void commitBatchSelect() { + assert isBeingBatchSelected() : "Batch was never started"; + if (!isBeingBatchSelected()) { + return; + } + + batchStarted = false; + + final Collection<T> added = getSelectedRowsBatch(); + selectionBatch.clear(); + + final Collection<T> removed = getDeselectedRowsBatch(); + deselectionBatch.clear(); + + grid.fireEvent(new SelectionChangeEvent<T>(grid, added, removed, + isBeingBatchSelected())); + } + + @Override + public boolean isBeingBatchSelected() { + return batchStarted; + } + + @Override + public Collection<T> getSelectedRowsBatch() { + return rowHandlesToRows(selectionBatch); + } + + @Override + public Collection<T> getDeselectedRowsBatch() { + return rowHandlesToRows(deselectionBatch); + } + + private ArrayList<T> rowHandlesToRows(Collection<RowHandle<T>> rowHandles) { + ArrayList<T> rows = new ArrayList<T>(rowHandles.size()); + for (RowHandle<T> handle : rowHandles) { + rows.add(handle.getRow()); + } + return rows; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java new file mode 100644 index 0000000000..8192237da0 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Renderer; + +/** + * No-row selection model. + * + * @author Vaadin Ltd + * @since + */ +public class SelectionModelNone<T> extends AbstractRowHandleSelectionModel<T> + implements SelectionModel.None<T> { + + @Override + public boolean isSelected(T row) { + return false; + } + + @Override + public Renderer<Boolean> getSelectionColumnRenderer() { + return null; + } + + @Override + public void setGrid(Grid<T> grid) { + // noop + } + + @Override + public void reset() { + // noop + } + + @Override + public Collection<T> getSelectedRows() { + return Collections.emptySet(); + } + + @Override + protected boolean selectByHandle(RowHandle<T> handle) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("This selection model " + + "does not support selection"); + } + + @Override + protected boolean deselectByHandle(RowHandle<T> handle) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("This selection model " + + "does not support deselection"); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java new file mode 100644 index 0000000000..d63b371c4d --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java @@ -0,0 +1,139 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.selection; + +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Single-row selection model. + * + * @author Vaadin Ltd + * @since + */ +public class SelectionModelSingle<T> extends AbstractRowHandleSelectionModel<T> + implements SelectionModel.Single<T> { + + private Grid<T> grid; + private RowHandle<T> selectedRow; + private Renderer<Boolean> renderer; + + @Override + public boolean isSelected(T row) { + return selectedRow != null + && selectedRow.equals(grid.getDataSource().getHandle(row)); + } + + @Override + public Renderer<Boolean> getSelectionColumnRenderer() { + return renderer; + } + + @Override + public void setGrid(Grid<T> grid) { + if (grid == null) { + throw new IllegalArgumentException("Grid cannot be null"); + } + + if (this.grid == null) { + this.grid = grid; + } else { + throw new IllegalStateException( + "Grid reference cannot be reassigned"); + } + renderer = new MultiSelectionRenderer<T>(grid); + } + + @Override + public boolean select(T row) { + + if (row == null) { + throw new IllegalArgumentException("Row cannot be null"); + } + + T removed = getSelectedRow(); + if (selectByHandle(grid.getDataSource().getHandle(row))) { + grid.fireEvent(new SelectionChangeEvent<T>(grid, row, removed, + false)); + + return true; + } + return false; + } + + @Override + public boolean deselect(T row) { + + if (row == null) { + throw new IllegalArgumentException("Row cannot be null"); + } + + if (isSelected(row)) { + deselectByHandle(selectedRow); + grid.fireEvent(new SelectionChangeEvent<T>(grid, null, row, false)); + return true; + } + + return false; + } + + @Override + public T getSelectedRow() { + return (selectedRow != null ? selectedRow.getRow() : null); + } + + @Override + public void reset() { + if (selectedRow != null) { + deselect(getSelectedRow()); + } + } + + @Override + public Collection<T> getSelectedRows() { + if (getSelectedRow() != null) { + return Collections.singleton(getSelectedRow()); + } + return Collections.emptySet(); + } + + @Override + protected boolean selectByHandle(RowHandle<T> handle) { + if (handle != null && !handle.equals(selectedRow)) { + deselectByHandle(selectedRow); + selectedRow = handle; + selectedRow.pin(); + return true; + } else { + return false; + } + } + + @Override + protected boolean deselectByHandle(RowHandle<T> handle) { + if (handle != null && handle.equals(selectedRow)) { + selectedRow.unpin(); + selectedRow = null; + return true; + } else { + return false; + } + } +} diff --git a/client/src/com/vaadin/client/ui/grid/sort/Sort.java b/client/src/com/vaadin/client/ui/grid/sort/Sort.java new file mode 100644 index 0000000000..00658c4375 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/sort/Sort.java @@ -0,0 +1,155 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.sort; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.client.ui.grid.GridColumn; +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Fluid Sort descriptor object. + * + * @since + * @author Vaadin Ltd + * @param T + * grid data type + */ +public class Sort { + + private final Sort previous; + private final SortOrder order; + private final int count; + + /** + * Basic constructor, used by the {@link #by(GridColumn)} and + * {@link #by(GridColumn, SortDirection)} methods. + * + * @param column + * a grid column + * @param direction + * a sort direction + */ + private Sort(GridColumn<?, ?> column, SortDirection direction) { + previous = null; + count = 1; + order = new SortOrder(column, direction); + } + + /** + * Extension constructor. Performs object equality checks on all previous + * Sort objects in the chain to make sure that the column being passed in + * isn't already used earlier (which would indicate a bug). If the column + * has been used before, this constructor throws an + * {@link IllegalStateException}. + * + * @param previous + * the sort instance that the new sort instance is to extend + * @param column + * a (previously unused) grid column reference + * @param direction + * a sort direction + */ + private Sort(Sort previous, GridColumn<?, ?> column, SortDirection direction) { + this.previous = previous; + count = previous.count + 1; + order = new SortOrder(column, direction); + + Sort s = previous; + while (s != null) { + if (s.order.getColumn() == column) { + throw new IllegalStateException( + "Can not sort along the same column twice"); + } + s = s.previous; + } + } + + /** + * Start building a Sort order by sorting a provided column in ascending + * order. + * + * @param column + * a grid column object reference + * @return a sort instance, typed to the grid data type + */ + public static Sort by(GridColumn<?, ?> column) { + return by(column, SortDirection.ASCENDING); + } + + /** + * Start building a Sort order by sorting a provided column. + * + * @param column + * a grid column object reference + * @param direction + * indicator of sort direction - either ascending or descending + * @return a sort instance, typed to the grid data type + */ + public static Sort by(GridColumn<?, ?> column, SortDirection direction) { + return new Sort(column, direction); + } + + /** + * Continue building a Sort order. The provided column is sorted in + * ascending order if the previously added columns have been evaluated as + * equals. + * + * @param column + * a grid column object reference + * @return a sort instance, typed to the grid data type + */ + public Sort then(GridColumn<?, ?> column) { + return then(column, SortDirection.ASCENDING); + } + + /** + * Continue building a Sort order. The provided column is sorted in + * specified order if the previously added columns have been evaluated as + * equals. + * + * @param column + * a grid column object reference + * @param direction + * indicator of sort direction - either ascending or descending + * @return a sort instance, typed to the grid data type + */ + public Sort then(GridColumn<?, ?> column, SortDirection direction) { + return new Sort(this, column, direction); + } + + /** + * Build a sort order list. This method is called internally by Grid when + * calling {@link com.vaadin.client.ui.grid.Grid#sort(Sort)}, but can also + * be called manually to create a SortOrder list, which can also be provided + * directly to Grid. + * + * @return a sort order list. + */ + public List<SortOrder> build() { + + List<SortOrder> order = new ArrayList<SortOrder>(count); + + Sort s = this; + for (int i = count - 1; i >= 0; --i) { + order.add(0, s.order); + s = s.previous; + } + + return order; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/sort/SortEvent.java b/client/src/com/vaadin/client/ui/grid/sort/SortEvent.java new file mode 100644 index 0000000000..edbd84c4a5 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/sort/SortEvent.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.sort; + +import java.util.List; + +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.shared.ui.grid.SortEventOriginator; + +/** + * A sort event, fired by the Grid when it needs its data source to provide data + * sorted in a specific manner. + * + * @since + * @author Vaadin Ltd + */ +public class SortEvent<T> extends GwtEvent<SortEventHandler<?>> { + + private static final Type<SortEventHandler<?>> TYPE = new Type<SortEventHandler<?>>(); + + private final Grid<T> grid; + private final List<SortOrder> order; + private final SortEventOriginator originator; + + /** + * Creates a new Sort Event. All provided parameters are final, and passed + * on as-is. + * + * @param grid + * a grid reference + * @param order + * an array dictating the desired sort order of the data source + * @param originator + * a value indicating where this event originated from + */ + public SortEvent(Grid<T> grid, List<SortOrder> order, + SortEventOriginator originator) { + this.grid = grid; + this.order = order; + this.originator = originator; + } + + @Override + public Type<SortEventHandler<?>> getAssociatedType() { + return TYPE; + } + + /** + * Static access to the GWT event type identifier associated with this Event + * class + * + * @return a type object, uniquely describing this event type. + */ + public static Type<SortEventHandler<?>> getType() { + return TYPE; + } + + /** + * Get access to the Grid that fired this event + * + * @return the grid instance + */ + @Override + public Grid<T> getSource() { + return grid; + } + + /** + * Get access to the Grid that fired this event + * + * @return the grid instance + */ + public Grid<T> getGrid() { + return grid; + } + + /** + * Access the data source of the Grid that fired this event + * + * @return a data source instance + */ + public DataSource<T> getDataSource() { + return grid.getDataSource(); + } + + /** + * Get the sort ordering that is to be applied to the Grid + * + * @return a list of sort order objects + */ + public List<SortOrder> getOrder() { + return order; + } + + /** + * Gets a value describing the originator of this event, i.e. what actions + * resulted in this event being fired. + * + * @return a sort event originator value + * + * @see SortEventOriginator + */ + public SortEventOriginator getOriginator() { + return originator; + } + + @SuppressWarnings("unchecked") + @Override + protected void dispatch(SortEventHandler<?> handler) { + ((SortEventHandler<T>) handler).sort(this); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java b/client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java new file mode 100644 index 0000000000..57e7fc2ead --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.sort; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for a Grid sort event, called when the Grid needs its data source to + * provide data sorted in a specific manner. + * + * @since + * @author Vaadin Ltd + */ +public interface SortEventHandler<T> extends EventHandler { + + /** + * Handle sorting of the Grid. This method is called when a re-sorting of + * the Grid's data is requested. + * + * @param event + * the sort event + */ + public void sort(SortEvent<T> event); + +} diff --git a/client/src/com/vaadin/client/ui/grid/sort/SortOrder.java b/client/src/com/vaadin/client/ui/grid/sort/SortOrder.java new file mode 100644 index 0000000000..682beda793 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/sort/SortOrder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid.sort; + +import com.vaadin.client.ui.grid.GridColumn; +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Sort order descriptor. Contains column and direction references. + * + * @since + * @author Vaadin Ltd + * @param T + * grid data type + */ +public class SortOrder { + + private final GridColumn<?, ?> column; + private final SortDirection direction; + + /** + * Create a sort order descriptor. + * + * @param column + * a grid column descriptor object + * @param direction + * a sorting direction value (ascending or descending) + */ + public SortOrder(GridColumn<?, ?> column, SortDirection direction) { + if (column == null) { + throw new IllegalArgumentException( + "Grid column reference can not be null!"); + } + if (direction == null) { + throw new IllegalArgumentException( + "Direction value can not be null!"); + } + this.column = column; + this.direction = direction; + } + + /** + * Returns the {@link GridColumn} reference given in the constructor. + * + * @return a grid column reference + */ + public GridColumn<?, ?> getColumn() { + return column; + } + + /** + * Returns the {@link SortDirection} value given in the constructor. + * + * @return a sort direction value + */ + public SortDirection getDirection() { + return direction; + } +} diff --git a/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java new file mode 100644 index 0000000000..55a2b56ee2 --- /dev/null +++ b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Comparator; + +import org.easymock.EasyMock; +import org.junit.Test; + +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.ui.grid.datasources.ListDataSource; + +public class ListDataSourceTest { + + @Test + public void testDataSourceConstruction() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + assertEquals(4, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + + ds = new ListDataSource<Integer>(Arrays.asList(0, 1, 2, 3)); + + assertEquals(4, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + } + + @Test + public void testListAddOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataAdded(4, 1); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().add(4); + + assertEquals(5, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + assertEquals(4, (int) ds.getRow(4)); + } + + @Test + public void testListAddAllOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataAdded(4, 3); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().addAll(Arrays.asList(4, 5, 6)); + + assertEquals(7, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + assertEquals(4, (int) ds.getRow(4)); + assertEquals(5, (int) ds.getRow(5)); + assertEquals(6, (int) ds.getRow(6)); + } + + @Test + public void testListRemoveOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataRemoved(3, 1); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().remove(2); + + assertEquals(3, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(3, (int) ds.getRow(2)); + } + + @Test + public void testListRemoveAllOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataRemoved(0, 3); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().removeAll(Arrays.asList(0, 2, 3)); + + assertEquals(1, ds.getEstimatedSize()); + assertEquals(1, (int) ds.getRow(0)); + } + + @Test + public void testListClearOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataRemoved(0, 4); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().clear(); + + assertEquals(0, ds.getEstimatedSize()); + } + + @Test(expected = IllegalStateException.class) + public void testFetchingNonExistantItem() { + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + ds.ensureAvailability(5, 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void testUnsupportedIteratorRemove() { + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + ds.asList().iterator().remove(); + } + + @Test + public void sortColumn() { + ListDataSource<Integer> ds = new ListDataSource<Integer>(3, 4, 2, 3, 1); + + // TODO Should be simplified to sort(). No point in providing these + // trivial comparators. + ds.sort(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + + assertTrue(Arrays.equals(ds.asList().toArray(), new Integer[] { 1, 2, + 3, 3, 4 })); + } + +} diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java new file mode 100644 index 0000000000..e97bb339e4 --- /dev/null +++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.ui.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.shared.ui.grid.Range; + +@SuppressWarnings("static-method") +public class PartitioningTest { + + @Test + public void selfRangeTest() { + final Range range = Range.between(0, 10); + final Range[] partitioning = range.partitionWith(range); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertTrue("inside is self", partitioning[1].equals(range)); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void beforeRangeTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(10, 20); + final Range[] partitioning = beforeRange.partitionWith(afterRange); + + assertTrue("before is self", partitioning[0].equals(beforeRange)); + assertTrue("inside is empty", partitioning[1].isEmpty()); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void afterRangeTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(10, 20); + final Range[] partitioning = afterRange.partitionWith(beforeRange); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertTrue("inside is empty", partitioning[1].isEmpty()); + assertTrue("after is self", partitioning[2].equals(afterRange)); + } + + @Test + public void beforeAndInsideRangeTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(5, 15); + final Range[] partitioning = beforeRange.partitionWith(afterRange); + + assertEquals("before", Range.between(0, 5), partitioning[0]); + assertEquals("inside", Range.between(5, 10), partitioning[1]); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void insideRangeTest() { + final Range fullRange = Range.between(0, 20); + final Range insideRange = Range.between(5, 15); + final Range[] partitioning = insideRange.partitionWith(fullRange); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertEquals("inside", Range.between(5, 15), partitioning[1]); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void insideAndBelowTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(5, 15); + final Range[] partitioning = afterRange.partitionWith(beforeRange); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertEquals("inside", Range.between(5, 10), partitioning[1]); + assertEquals("after", Range.between(10, 15), partitioning[2]); + } + + @Test + public void aboveAndBelowTest() { + final Range fullRange = Range.between(0, 20); + final Range insideRange = Range.between(5, 15); + final Range[] partitioning = fullRange.partitionWith(insideRange); + + assertEquals("before", Range.between(0, 5), partitioning[0]); + assertEquals("inside", Range.between(5, 15), partitioning[1]); + assertEquals("after", Range.between(15, 20), partitioning[2]); + } +} diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java index 8e99bac541..c58d37d5fe 100644 --- a/server/src/com/vaadin/data/Container.java +++ b/server/src/com/vaadin/data/Container.java @@ -582,6 +582,60 @@ public interface Container extends Serializable { public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException; + /** + * An <code>Event</code> object specifying information about the added + * items. + */ + public interface ItemAddEvent extends ItemSetChangeEvent { + + /** + * Gets the item id of the first added item. + * + * @return item id of the first added item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first added item. + * + * @return index of the first added item + */ + public int getFirstIndex(); + + /** + * Gets the number of the added items. + * + * @return the number of added items. + */ + public int getAddedItemsCount(); + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + */ + public interface ItemRemoveEvent extends ItemSetChangeEvent { + /** + * Gets the item id of the first removed item. + * + * @return item id of the first removed item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first removed item. + * + * @return index of the first removed item + */ + public int getFirstIndex(); + + /** + * Gets the number of the removed items. + * + * @return the number of removed items + */ + public int getRemovedItemsCount(); + } } /** diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java new file mode 100644 index 0000000000..86fa11f62e --- /dev/null +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -0,0 +1,936 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.google.gwt.thirdparty.guava.common.collect.BiMap; +import com.google.gwt.thirdparty.guava.common.collect.HashBiMap; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.server.AbstractExtension; +import com.vaadin.server.ClientConnector; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; +import com.vaadin.ui.components.grid.Renderer; +import com.vaadin.ui.components.grid.selection.SelectionChangeEvent; +import com.vaadin.ui.components.grid.selection.SelectionChangeListener; + +/** + * Provides Vaadin server-side container data source to a + * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently + * implemented as an Extension hardcoded to support a specific connector type. + * This will be changed once framework support for something more flexible has + * been implemented. + * + * @since + * @author Vaadin Ltd + */ +public class RpcDataProviderExtension extends AbstractExtension { + + /** + * ItemId to Key to ItemId mapper. + * <p> + * This class is used when transmitting information about items in container + * related to Grid. It introduces a consistent way of mapping ItemIds and + * its container to a String that can be mapped back to ItemId. + * <p> + * <em>Technical note:</em> This class also keeps tabs on which indices are + * being shown/selected, and is able to clean up after itself once the + * itemId ⇆ key mapping is not needed anymore. In other words, this + * doesn't leak memory. + */ + public class DataProviderKeyMapper implements Serializable { + private final BiMap<Integer, Object> indexToItemId = HashBiMap.create(); + private final BiMap<Object, String> itemIdToKey = HashBiMap.create(); + private Set<Object> pinnedItemIds = new HashSet<Object>(); + private Range activeRange = Range.withLength(0, 0); + private long rollingIndex = 0; + + private DataProviderKeyMapper() { + // private implementation + } + + void preActiveRowsChange(Range newActiveRange, int firstNewIndex, + List<?> itemIds) { + final Range[] removed = activeRange.partitionWith(newActiveRange); + final Range[] added = newActiveRange.partitionWith(activeRange); + + removeActiveRows(removed[0]); + removeActiveRows(removed[2]); + addActiveRows(added[0], firstNewIndex, itemIds); + addActiveRows(added[2], firstNewIndex, itemIds); + + activeRange = newActiveRange; + } + + private void removeActiveRows(final Range deprecated) { + for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) { + final Integer ii = Integer.valueOf(i); + final Object itemId = indexToItemId.get(ii); + + if (!isPinned(itemId)) { + itemIdToKey.remove(itemId); + indexToItemId.remove(ii); + } + } + } + + private void addActiveRows(final Range added, int firstNewIndex, + List<?> newItemIds) { + + for (int i = added.getStart(); i < added.getEnd(); i++) { + + /* + * We might be in a situation we have an index <-> itemId entry + * already. This happens when something was selected, scrolled + * out of view and now we're scrolling it back into view. It's + * unnecessary to overwrite it in that case. + * + * Fun thought: considering branch prediction, it _might_ even + * be a bit faster to simply always run the code beyond this + * if-state. But it sounds too stupid (and most often too + * insignificant) to try out. + */ + final Integer ii = Integer.valueOf(i); + if (indexToItemId.containsKey(ii)) { + continue; + } + + /* + * We might be in a situation where we have an itemId <-> key + * entry already, but no index for it. This happens when + * something that is out of view is selected programmatically. + * In that case, we only want to add an index for that entry, + * and not overwrite the key. + */ + final Object itemId = newItemIds.get(i - firstNewIndex); + if (!itemIdToKey.containsKey(itemId)) { + itemIdToKey.put(itemId, nextKey()); + } + indexToItemId.put(ii, itemId); + } + } + + private String nextKey() { + return String.valueOf(rollingIndex++); + } + + String getKey(Object itemId) { + String key = itemIdToKey.get(itemId); + if (key == null) { + key = nextKey(); + itemIdToKey.put(itemId, key); + } + return key; + } + + /** + * Gets keys for a collection of item ids. + * <p> + * If the itemIds are currently cached, the existing keys will be used. + * Otherwise new ones will be created. + * + * @param itemIds + * the item ids for which to get keys + * @return keys for the {@code itemIds} + */ + public List<String> getKeys(Collection<Object> itemIds) { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds can't be null"); + } + + ArrayList<String> keys = new ArrayList<String>(itemIds.size()); + for (Object itemId : itemIds) { + keys.add(getKey(itemId)); + } + return keys; + } + + /** + * Gets the registered item id based on its key. + * <p> + * A key is used to identify a particular row on both a server and a + * client. This method can be used to get the item id for the row key + * that the client has sent. + * + * @param key + * the row key for which to retrieve an item id + * @return the item id corresponding to {@code key} + * @throws IllegalStateException + * if the key mapper does not have a record of {@code key} . + */ + public Object getItemId(String key) throws IllegalStateException { + Object itemId = itemIdToKey.inverse().get(key); + if (itemId != null) { + return itemId; + } else { + throw new IllegalStateException("No item id for key " + key + + " found."); + } + } + + /** + * Gets corresponding item ids for each of the keys in a collection. + * + * @param keys + * the keys for which to retrieve item ids + * @return a collection of item ids for the {@code keys} + * @throws IllegalStateException + * if one or more of keys don't have a corresponding item id + * in the cache + */ + public Collection<Object> getItemIds(Collection<String> keys) + throws IllegalStateException { + if (keys == null) { + throw new IllegalArgumentException("keys may not be null"); + } + + ArrayList<Object> itemIds = new ArrayList<Object>(keys.size()); + for (String key : keys) { + itemIds.add(getItemId(key)); + } + return itemIds; + } + + /** + * Pin an item id to be cached indefinitely. + * <p> + * Normally when an itemId is not an active row, it is discarded from + * the cache. Pinning an item id will make sure that it is kept in the + * cache. + * <p> + * In effect, while an item id is pinned, it always has the same key. + * + * @param itemId + * the item id to pin + * @throws IllegalStateException + * if {@code itemId} was already pinned + * @see #unpin(Object) + * @see #isPinned(Object) + * @see #getItemIds(Collection) + */ + public void pin(Object itemId) throws IllegalStateException { + if (isPinned(itemId)) { + throw new IllegalStateException("Item id " + itemId + + " was pinned already"); + } + pinnedItemIds.add(itemId); + } + + /** + * Unpin an item id. + * <p> + * This cancels the effect of pinning an item id. If the item id is + * currently inactive, it will be immediately removed from the cache. + * + * @param itemId + * the item id to unpin + * @throws IllegalStateException + * if {@code itemId} was not pinned + * @see #pin(Object) + * @see #isPinned(Object) + * @see #getItemIds(Collection) + */ + public void unpin(Object itemId) throws IllegalStateException { + if (!isPinned(itemId)) { + throw new IllegalStateException("Item id " + itemId + + " was not pinned"); + } + + pinnedItemIds.remove(itemId); + final Integer index = indexToItemId.inverse().get(itemId); + if (index == null || !activeRange.contains(index.intValue())) { + itemIdToKey.remove(itemId); + indexToItemId.remove(index); + } + } + + /** + * Checks whether an item id is pinned or not. + * + * @param itemId + * the item id to check for pin status + * @return {@code true} iff the item id is currently pinned + */ + public boolean isPinned(Object itemId) { + return pinnedItemIds.contains(itemId); + } + + Object itemIdAtIndex(int index) { + return indexToItemId.inverse().get(Integer.valueOf(index)); + } + } + + /** + * A helper class that handles the client-side Escalator logic relating to + * making sure that whatever is currently visible to the user, is properly + * initialized and otherwise handled on the server side (as far as + * required). + * <p> + * This bookeeping includes, but is not limited to: + * <ul> + * <li>listening to the currently visible {@link com.vaadin.data.Property + * Properties'} value changes on the server side and sending those back to + * the client; and + * <li>attaching and detaching {@link com.vaadin.ui.Component Components} + * from the Vaadin Component hierarchy. + * </ul> + */ + private class ActiveRowHandler implements Serializable { + /** + * A map from itemId to the value change listener used for all of its + * properties + */ + private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>(); + + /** + * The currently active range. Practically, it's the range of row + * indices being cached currently. + */ + private Range activeRange = Range.withLength(0, 0); + + /** + * A hook for making sure that appropriate data is "active". All other + * rows should be "inactive". + * <p> + * "Active" can mean different things in different contexts. For + * example, only the Properties in the active range need + * ValueChangeListeners. Also, whenever a row with a Component becomes + * active, it needs to be attached (and conversely, when inactive, it + * needs to be detached). + * + * @param firstActiveRow + * the first active row + * @param activeRowCount + * the number of active rows + */ + public void setActiveRows(int firstActiveRow, int activeRowCount) { + + final Range newActiveRange = Range.withLength(firstActiveRow, + activeRowCount); + + // TODO [[Components]] attach and detach components + + /*- + * Example + * + * New Range: [3, 4, 5, 6, 7] + * Old Range: [1, 2, 3, 4, 5] + * Result: [1, 2][3, 4, 5] [] + */ + final Range[] depractionPartition = activeRange + .partitionWith(newActiveRange); + removeValueChangeListeners(depractionPartition[0]); + removeValueChangeListeners(depractionPartition[2]); + + /*- + * Example + * + * Old Range: [1, 2, 3, 4, 5] + * New Range: [3, 4, 5, 6, 7] + * Result: [] [3, 4, 5][6, 7] + */ + final Range[] activationPartition = newActiveRange + .partitionWith(activeRange); + addValueChangeListeners(activationPartition[0]); + addValueChangeListeners(activationPartition[2]); + + activeRange = newActiveRange; + } + + private void addValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + + final Object itemId = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + + if (valueChangeListeners.containsKey(itemId)) { + /* + * This might occur when items are removed from above the + * viewport, the escalator scrolls up to compensate, but the + * same items remain in the view: It looks as if one row was + * scrolled, when in fact the whole viewport was shifted up. + */ + continue; + } + + GridValueChangeListener listener = new GridValueChangeListener( + itemId); + valueChangeListeners.put(itemId, listener); + + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(@SuppressWarnings("unused") + Collection<Object> removedPropertyIds) { + /* + * no-op, for now. + * + * The Container should be responsible for cleaning out any + * ValueChangeListeners from removed Properties. Components will + * benefit from this, however. + */ + } + + /** + * Manages added properties in active rows. + * + * @param addedPropertyIds + * the property ids that have been added to the container + */ + public void propertiesAdded(Collection<Object> addedPropertyIds) { + for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { + final Object itemId = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .get(itemId); + assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; + + for (final Object propertyId : addedPropertyIds) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + /** + * Handles the insertion of rows. + * <p> + * This method's responsibilities are to: + * <ul> + * <li>shift the internal bookkeeping by <code>count</code> if the + * insertion happens above currently active range + * <li>ignore rows inserted below the currently active range + * <li>shift (and deactivate) rows pushed out of view + * <li>activate rows that are inserted in the current viewport + * </ul> + * + * @param firstIndex + * the index of the first inserted rows + * @param count + * the number of rows inserted at <code>firstIndex</code> + */ + public void insertRows(int firstIndex, int count) { + if (firstIndex < activeRange.getStart()) { + activeRange = activeRange.offsetBy(count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecatedRange = Range.withLength( + activeRange.getEnd(), count); + removeValueChangeListeners(deprecatedRange); + + final Range freshRange = Range.between(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Removes a single item by its id. + * + * @param itemId + * the id of the removed id. <em>Note:</em> this item does + * not exist anymore in the datasource + */ + public void removeItemId(Object itemId) { + final GridValueChangeListener removedListener = valueChangeListeners + .remove(itemId); + if (removedListener != null) { + /* + * We removed an item from somewhere in the visible range, so we + * make the active range shorter. The empty hole will be filled + * by the client-side code when it asks for more information. + */ + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - 1); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + * <p> + * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + * <p> + * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + + public GridValueChangeListener(Object itemId) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + } + + @Override + public void valueChange(ValueChangeEvent event) { + updateRowData(container.indexOfId(itemId)); + } + } + + private final Indexed container; + + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + + private final ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + insertRowData(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + removeRowData(firstIndex, count); + } + + else { + Range visibleRows = activeRowHandler.activeRange; + List<?> itemIds = container.getItemIds(visibleRows.getStart(), + visibleRows.length()); + + keyMapper.removeActiveRows(keyMapper.activeRange); + keyMapper.addActiveRows(visibleRows, visibleRows.getStart(), + itemIds); + + pushRows(visibleRows.getStart(), itemIds); + activeRowHandler.setActiveRows(visibleRows.getStart(), + visibleRows.length()); + } + } + }; + + private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper(); + + /** + * Creates a new data provider using the given container. + * + * @param container + * the container to make available + */ + public RpcDataProviderExtension(Indexed container) { + this.container = container; + + registerRpc(new DataRequestRpc() { + private Collection<String> allTemporarilyPinnedKeys = new ArrayList<String>(); + + @Override + public void requestRows(int firstRow, int numberOfRows, + int firstCachedRowIndex, int cacheSize, + List<String> temporarilyPinnedKeys) { + + for (String key : temporarilyPinnedKeys) { + Object itemId = keyMapper.getItemId(key); + if (!keyMapper.isPinned(itemId)) { + keyMapper.pin(itemId); + } + } + allTemporarilyPinnedKeys.addAll(temporarilyPinnedKeys); + + Range active = Range.withLength(firstRow, numberOfRows); + if (cacheSize != 0) { + Range cached = Range.withLength(firstCachedRowIndex, + cacheSize); + active = active.combineWith(cached); + } + + List<?> itemIds = RpcDataProviderExtension.this.container + .getItemIds(firstRow, numberOfRows); + keyMapper.preActiveRowsChange(active, firstRow, itemIds); + pushRows(firstRow, itemIds); + + activeRowHandler.setActiveRows(active.getStart(), + active.length()); + } + + @Override + public void releaseTemporarilyPinnedKeys() { + /* + * This needs to be done deferredly since the selection event + * comes after this RPC call. + */ + + final SelectionChangeListener listener = new SelectionChangeListener() { + @Override + public void selectionChange(SelectionChangeEvent event) { + for (String tempPinnedKey : allTemporarilyPinnedKeys) { + /* + * TODO: this could be moved into a field instead of + * inline to reduce indentations. + */ + + /* + * This works around the fact that when deselecting + * and leaping through the cache, the client tries + * to send a deselect event even though a row never + * was selected. So, it tries to unpin something + * that never was temporarily pinned. + * + * If the same thing would happen while selecting + * (instead of deselecting), the row would be + * pinned, not because of the temporary pinning, but + * because it's selected. + */ + if (!keyMapper.isPinned(tempPinnedKey)) { + continue; + } + + Object itemId = keyMapper.getItemId(tempPinnedKey); + Integer index = keyMapper.indexToItemId.inverse() + .get(itemId); + if (!getGrid().isSelected(itemId) + && !activeRowHandler.activeRange + .contains(index.intValue())) { + keyMapper.unpin(itemId); + } + } + allTemporarilyPinnedKeys = new ArrayList<String>(); + getGrid().removeSelectionChangeListener(this); + } + }; + + getGrid().addSelectionChangeListener(listener); + } + }); + + getState().containerSize = container.size(); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .addItemSetChangeListener(itemListener); + } + + } + + private void pushRows(int firstRow, List<?> itemIds) { + Collection<?> propertyIds = container.getContainerPropertyIds(); + JSONArray rows = new JSONArray(); + for (Object itemId : itemIds) { + rows.put(getRowData(propertyIds, itemId)); + } + String jsonString = rows.toString(); + getRpcProxy(DataProviderRpc.class).setRowData(firstRow, jsonString); + } + + private JSONObject getRowData(Collection<?> propertyIds, Object itemId) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + JSONArray rowData = new JSONArray(); + + Grid grid = getGrid(); + try { + for (Object propertyId : propertyIds) { + GridColumn column = grid.getColumn(propertyId); + + Object propertyValue = item.getItemProperty(propertyId) + .getValue(); + Object encodedValue = encodeValue(propertyValue, + column.getRenderer(), column.getConverter(), + grid.getLocale()); + + rowData.put(encodedValue); + } + + final JSONObject rowObject = new JSONObject(); + rowObject.put(GridState.JSONKEY_DATA, rowData); + rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId)); + return rowObject; + } catch (final JSONException e) { + throw new RuntimeException("Grid was unable to serialize " + + "data for row (this should've been caught " + + "eariler by other Grid logic)", e); + } + } + + @Override + protected DataProviderState getState() { + return (DataProviderState) super.getState(); + } + + /** + * Makes the data source available to the given {@link Grid} component. + * + * @param component + * the remote data grid component to extend + */ + public void extend(Grid component) { + super.extend(component); + } + + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at <code>index</code> + */ + private void insertRowData(int index, int count) { + getState().containerSize += count; + getRpcProxy(DataProviderRpc.class).insertRowData(index, count); + + activeRowHandler.insertRows(index, count); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param firstIndex + * the index of the first row removed + * @param count + * the number of rows removed + * @param firstItemId + * the item id of the first removed item + */ + private void removeRowData(int firstIndex, int count) { + getState().containerSize -= count; + getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); + + for (int i = 0; i < count; i++) { + Object itemId = keyMapper.itemIdAtIndex(firstIndex + i); + if (itemId != null) { + activeRowHandler.removeItemId(itemId); + } + } + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param index + * the index of the row that was updated + */ + public void updateRowData(int index) { + /* + * TODO: ignore duplicate requests for the same index during the same + * roundtrip. + */ + Object itemId = container.getIdByIndex(index); + JSONObject row = getRowData(container.getContainerPropertyIds(), itemId); + JSONArray rowArray = new JSONArray(Collections.singleton(row)); + String jsonString = rowArray.toString(); + getRpcProxy(DataProviderRpc.class).setRowData(index, jsonString); + } + + @Override + public void setParent(ClientConnector parent) { + super.setParent(parent); + if (parent == null) { + // We're detached, release various listeners + + activeRowHandler + .removeValueChangeListeners(activeRowHandler.activeRange); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .removeItemSetChangeListener(itemListener); + } + + } + } + + /** + * Informs this data provider that some of the properties have been removed + * from the container. + * <p> + * Please note that we could add our own + * {@link com.vaadin.data.Container.PropertySetChangeListener + * PropertySetChangeListener} to the container, but then we'd need to + * implement the same bookeeping for finding what's added and removed that + * Grid already does in its own listener. + * + * @param removedColumns + * a list of property ids for the removed columns + */ + public void propertiesRemoved(List<Object> removedColumns) { + activeRowHandler.propertiesRemoved(removedColumns); + } + + /** + * Informs this data provider that some of the properties have been added to + * the container. + * <p> + * Please note that we could add our own + * {@link com.vaadin.data.Container.PropertySetChangeListener + * PropertySetChangeListener} to the container, but then we'd need to + * implement the same bookeeping for finding what's added and removed that + * Grid already does in its own listener. + * + * @param addedPropertyIds + * a list of property ids for the added columns + */ + public void propertiesAdded(HashSet<Object> addedPropertyIds) { + activeRowHandler.propertiesAdded(addedPropertyIds); + } + + public DataProviderKeyMapper getKeyMapper() { + return keyMapper; + } + + protected Grid getGrid() { + return (Grid) getParent(); + } + + /** + * Converts and encodes the given data model property value using the given + * converter and renderer. This method is public only for testing purposes. + * + * @param renderer + * the renderer to use + * @param converter + * the converter to use + * @param modelValue + * the value to convert and encode + * @param locale + * the locale to use in conversion + * @return an encoded value ready to be sent to the client + */ + public static <T> Object encodeValue(Object modelValue, + Renderer<T> renderer, Converter<?, ?> converter, Locale locale) { + Class<T> presentationType = renderer.getPresentationType(); + T presentationValue; + + if (converter == null) { + try { + presentationValue = presentationType.cast(modelValue); + } catch (ClassCastException e) { + throw new Converter.ConversionException( + "Unable to convert value of type " + + modelValue.getClass().getName() + + " to presentation type " + + presentationType.getName() + + ". No converter is set and the types are not compatible."); + } + } else { + assert presentationType.isAssignableFrom(converter + .getPresentationType()); + @SuppressWarnings("unchecked") + Converter<T, Object> safeConverter = (Converter<T, Object>) converter; + presentationValue = safeConverter.convertToPresentation(modelValue, + safeConverter.getPresentationType(), locale); + } + + Object encodedValue = renderer.encode(presentationValue); + + /* + * because this is a relatively heavy operation, we'll hide this behind + * an assert so that the check will be removed in production mode + */ + assert jsonSupports(encodedValue) : "org.json.JSONObject does not know how to serialize objects of type " + + encodedValue.getClass().getName(); + return encodedValue; + } + + private static boolean jsonSupports(Object encodedValue) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.accumulate("test", encodedValue); + } catch (JSONException e) { + return false; + } + return true; + } +} diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index adf6313770..0559585e14 100644 --- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -222,6 +222,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends @Override public boolean removeAllItems() { int origSize = size(); + IDTYPE firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -234,7 +235,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends // fire event only if the visible view changed, regardless of whether // filtered out items were removed or not if (origSize != 0) { - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -679,6 +680,8 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends protected void addAll(Collection<? extends BEANTYPE> collection) throws IllegalStateException, IllegalArgumentException { boolean modified = false; + int origSize = size(); + for (BEANTYPE bean : collection) { // TODO skipping invalid beans - should not allow them in javadoc? if (bean == null @@ -699,13 +702,22 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends if (modified) { // Filter the contents when all items have been added if (isFiltered()) { - filterAll(); - } else { - fireItemSetChange(); + doFilterContainer(!getFilters().isEmpty()); + } + if (visibleNewItemsWasAdded(origSize)) { + // fire event about added items + int firstPosition = origSize; + IDTYPE firstItemId = getVisibleItemIds().get(firstPosition); + int affectedItems = size() - origSize; + fireItemsAdded(firstPosition, firstItemId, affectedItems); } } } + private boolean visibleNewItemsWasAdded(int origSize) { + return size() > origSize; + } + /** * Use the bean resolver to get the identifier for a bean. * diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java index b19cddb021..5ddc11ec6f 100644 --- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java +++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -15,8 +15,10 @@ */ package com.vaadin.data.util; +import java.io.Serializable; import java.util.Collection; import java.util.Collections; +import java.util.EventObject; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -146,6 +148,85 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } } + private static abstract class BaseItemAddOrRemoveEvent extends + EventObject implements Serializable { + protected Object itemId; + protected int index; + protected int count; + + public BaseItemAddOrRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source); + this.itemId = itemId; + this.index = index; + this.count = count; + } + + public Container getContainer() { + return (Container) getSource(); + } + + public Object getFirstItemId() { + return itemId; + } + + public int getFirstIndex() { + return index; + } + + public int getAffectedItemsCount() { + return count; + } + } + + /** + * An <code>Event</code> object specifying information about the added + * items. + * + * <p> + * This class provides information about the first added item and the number + * of added items. + * </p> + */ + protected static class BaseItemAddEvent extends + BaseItemAddOrRemoveEvent implements + Container.Indexed.ItemAddEvent { + + public BaseItemAddEvent(Container source, Object itemId, int index, + int count) { + super(source, itemId, index, count); + } + + @Override + public int getAddedItemsCount() { + return getAffectedItemsCount(); + } + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + * + * <p> + * This class provides information about the first removed item and the + * number of removed items. + * </p> + */ + protected static class BaseItemRemoveEvent extends + BaseItemAddOrRemoveEvent implements + Container.Indexed.ItemRemoveEvent { + + public BaseItemRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source, itemId, index, count); + } + + @Override + public int getRemovedItemsCount() { + return getAffectedItemsCount(); + } + } + /** * Get an item even if filtered out. * @@ -898,36 +979,69 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE * Notify item set change listeners that an item has been added to the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. - * * @param postion - * position of the added item in the view (if visible) + * position of the added item in the view * @param itemId * id of the added item * @param item * the added item */ protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) { - fireItemSetChange(); + fireItemsAdded(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been added to the + * container. + * + * @param firstPosition + * position of the first visible added item in the view + * @param firstItemId + * id of the first visible added item + * @param numberOfItems + * the number of visible added items + */ + protected void fireItemsAdded(int firstPosition, ITEMIDTYPE firstItemId, + int numberOfItems) { + BaseItemAddEvent addEvent = new BaseItemAddEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(addEvent); } /** * Notify item set change listeners that an item has been removed from the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. + * @param position + * position of the removed item in the view prior to removal * - * @param postion - * position of the removed item in the view prior to removal (if - * was visible) * @param itemId * id of the removed item, of type {@link Object} to satisfy * {@link Container#removeItem(Object)} API */ protected void fireItemRemoved(int position, Object itemId) { - fireItemSetChange(); + fireItemsRemoved(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been removed from the + * container. + * + * @param firstPosition + * position of the first visible removed item in the view prior + * to removal + * @param firstItemId + * id of the first visible removed item, of type {@link Object} + * to satisfy {@link Container#removeItem(Object)} API + * @param numberOfItems + * the number of removed visible items + * + */ + protected void fireItemsRemoved(int firstPosition, Object firstItemId, + int numberOfItems) { + BaseItemRemoveEvent removeEvent = new BaseItemRemoveEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(removeEvent); } // visible and filtered item identifier lists @@ -946,6 +1060,21 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } /** + * Returns the item id of the first visible item after filtering. 'Null' is + * returned if there is no visible items. + * + * For internal use only. + * + * @return item id of the first visible item + */ + protected ITEMIDTYPE getFirstVisibleItem() { + if (!getVisibleItemIds().isEmpty()) { + return getVisibleItemIds().get(0); + } + return null; + } + + /** * Returns true is the container has active filters. * * @return true if the container is currently filtered diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java index 68960335d7..f9cc4c482a 100644 --- a/server/src/com/vaadin/data/util/IndexedContainer.java +++ b/server/src/com/vaadin/data/util/IndexedContainer.java @@ -226,6 +226,7 @@ public class IndexedContainer extends @Override public boolean removeAllItems() { int origSize = size(); + Object firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -235,7 +236,7 @@ public class IndexedContainer extends // filtered out items were removed or not if (origSize != 0) { // Sends a change event - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -620,8 +621,7 @@ public class IndexedContainer extends @Override protected void fireItemAdded(int position, Object itemId, Item item) { if (position >= 0) { - fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, - position)); + super.fireItemAdded(position, itemId, item); } } @@ -1211,4 +1211,5 @@ public class IndexedContainer extends public Collection<Filter> getContainerFilters() { return super.getContainerFilters(); } + } diff --git a/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java new file mode 100644 index 0000000000..d1cf77c24b --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.server.AbstractClientConnector; +import com.vaadin.server.AbstractExtension; + +/** + * An abstract base class for server-side Grid renderers. + * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. This class + * currently extends the AbstractExtension superclass, but this fact should be + * regarded as an implementation detail and subject to change in a future major + * or minor Vaadin revision. + * + * @param <T> + * the type this renderer knows how to present + * + * @since + * @author Vaadin Ltd + */ +public abstract class AbstractRenderer<T> extends AbstractExtension implements + Renderer<T> { + + private final Class<T> presentationType; + + protected AbstractRenderer(Class<T> presentationType) { + this.presentationType = presentationType; + } + + /** + * This method is inherited from AbstractExtension but should never be + * called directly with an AbstractRenderer. + */ + @Deprecated + @Override + protected Class<Grid> getSupportedParentType() { + return Grid.class; + } + + /** + * This method is inherited from AbstractExtension but should never be + * called directly with an AbstractRenderer. + */ + @Deprecated + @Override + protected void extend(AbstractClientConnector target) { + super.extend(target); + } + + @Override + public Class<T> getPresentationType() { + return presentationType; + } + + /** + * Gets the item id for a row key. + * <p> + * A key is used to identify a particular row on both a server and a client. + * This method can be used to get the item id for the row key that the + * client has sent. + * + * @param key + * the row key for which to retrieve an item id + * @return the item id corresponding to {@code key} + */ + protected Object getItemId(String key) { + if (getParent() instanceof Grid) { + Grid grid = (Grid) getParent(); + return grid.getKeyMapper().getItemId(key); + } else { + throw new IllegalStateException( + "Renderers can be used only with Grid"); + } + } +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java new file mode 100644 index 0000000000..3c115f9241 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -0,0 +1,1307 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui.components.grid; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView; +import com.vaadin.data.Container; +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Container.Sortable; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridClientRpc; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.SortDirection; +import com.vaadin.shared.ui.grid.SortEventOriginator; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.components.grid.GridFooter.FooterCell; +import com.vaadin.ui.components.grid.GridFooter.FooterRow; +import com.vaadin.ui.components.grid.GridHeader.HeaderCell; +import com.vaadin.ui.components.grid.GridHeader.HeaderRow; +import com.vaadin.ui.components.grid.selection.MultiSelectionModel; +import com.vaadin.ui.components.grid.selection.NoSelectionModel; +import com.vaadin.ui.components.grid.selection.SelectionChangeEvent; +import com.vaadin.ui.components.grid.selection.SelectionChangeListener; +import com.vaadin.ui.components.grid.selection.SelectionChangeNotifier; +import com.vaadin.ui.components.grid.selection.SelectionModel; +import com.vaadin.ui.components.grid.selection.SingleSelectionModel; +import com.vaadin.ui.components.grid.sort.Sort; +import com.vaadin.ui.components.grid.sort.SortOrder; +import com.vaadin.util.ReflectTools; + +/** + * A grid component for displaying tabular data. + * <p> + * Grid is always bound to a {@link Container.Indexed}, but is not a + * {@code Container} of any kind in of itself. The contents of the given + * Container is displayed with the help of {@link Renderer Renderers}. + * + * <h3 id="grid-headers-and-footers">Headers and Footers</h3> + * <p> + * + * + * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3> + * <p> + * Each column has its own {@link Renderer} that displays data into something + * that can be displayed in the browser. That data is first converted with a + * {@link com.vaadin.data.util.converter.Converter Converter} into something + * that the Renderer can process. This can also be an implicit step - if a + * column has a simple data type, like a String, no explicit assignment is + * needed. + * <p> + * Usually a renderer takes some kind of object, and converts it into a + * HTML-formatted string. + * <p> + * <code><pre> + * Grid grid = new Grid(myContainer); + * GridColumn column = grid.getColumn(STRING_DATE_PROPERTY); + * column.setConverter(new StringToDateConverter()); + * column.setRenderer(new MyColorfulDateRenderer()); + * </pre></code> + * + * <h3 id="grid-lazyloading">Lazy Loading</h3> + * <p> + * The data is accessed as it is needed by Grid and not any sooner. In other + * words, if the given Container is huge, but only the first few rows are + * displayed to the user, only those (and a few more, for caching purposes) are + * accessed. + * + * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3> + * <p> + * Grid supports three selection <em>{@link SelectionMode modes}</em> (single, + * multi, none), and comes bundled with one + * <em>{@link SelectionModel model}</em> for each of the modes. The distinction + * between a selection mode and selection model is as follows: a <em>mode</em> + * essentially says whether you can have one, many or no rows selected. The + * model, however, has the behavioral details of each. A single selection model + * may require that the user deselects one row before selecting another one. A + * variant of a multiselect might have a configurable maximum of rows that may + * be selected. And so on. + * <p> + * <code><pre> + * Grid grid = new Grid(myContainer); + * + * // uses the bundled SingleSelectionModel class + * grid.setSelectionMode(SelectionMode.SINGLE); + * + * // changes the behavior to a custom selection model + * grid.setSelectionModel(new MyTwoSelectionModel()); + * </pre></code> + * + * @since + * @author Vaadin Ltd + */ +public class Grid extends AbstractComponent implements SelectionChangeNotifier, + HasComponents { + + /** + * Selection modes representing built-in {@link SelectionModel + * SelectionModels} that come bundled with {@link Grid}. + * <p> + * Passing one of these enums into + * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling + * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in + * implementations of {@link SelectionModel}. + * + * @see Grid#setSelectionMode(SelectionMode) + * @see Grid#setSelectionModel(SelectionModel) + */ + public enum SelectionMode { + /** A SelectionMode that maps to {@link SingleSelectionModel} */ + SINGLE { + @Override + protected SelectionModel createModel() { + return new SingleSelectionModel(); + } + + }, + + /** A SelectionMode that maps to {@link MultiSelectionModel} */ + MULTI { + @Override + protected SelectionModel createModel() { + return new MultiSelectionModel(); + } + }, + + /** A SelectionMode that maps to {@link NoSelectionModel} */ + NONE { + @Override + protected SelectionModel createModel() { + return new NoSelectionModel(); + } + }; + + protected abstract SelectionModel createModel(); + } + + /** + * The data source attached to the grid + */ + private Container.Indexed datasource; + + /** + * Property id to column instance mapping + */ + private final Map<Object, GridColumn> columns = new HashMap<Object, GridColumn>(); + + /** + * Key generator for column server-to-client communication + */ + private final KeyMapper<Object> columnKeys = new KeyMapper<Object>(); + + /** + * The current sort order + */ + private final List<SortOrder> sortOrder = new ArrayList<SortOrder>(); + + /** + * Property listener for listening to changes in data source properties. + */ + private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + Collection<?> properties = new HashSet<Object>(event.getContainer() + .getContainerPropertyIds()); + + // Cleanup columns that are no longer in grid + List<Object> removedColumns = new LinkedList<Object>(); + for (Object columnId : columns.keySet()) { + if (!properties.contains(columnId)) { + removedColumns.add(columnId); + } + } + for (Object columnId : removedColumns) { + GridColumn column = columns.remove(columnId); + columnKeys.remove(columnId); + getState().columns.remove(column.getState()); + removeExtension(column.getRenderer()); + } + datasourceExtension.propertiesRemoved(removedColumns); + + // Add new columns + HashSet<Object> addedPropertyIds = new HashSet<Object>(); + for (Object propertyId : properties) { + if (!columns.containsKey(propertyId)) { + appendColumn(propertyId); + addedPropertyIds.add(propertyId); + } + } + datasourceExtension.propertiesAdded(addedPropertyIds); + + Object frozenPropertyId = columnKeys + .get(getState(false).lastFrozenColumnId); + if (!columns.containsKey(frozenPropertyId)) { + setLastFrozenPropertyId(null); + } + } + }; + + private RpcDataProviderExtension datasourceExtension; + + /** + * The selection model that is currently in use. Never <code>null</code> + * after the constructor has been run. + */ + private SelectionModel selectionModel; + + /** + * The number of times to ignore selection state sync to the client. + * <p> + * This usually means that the client side has modified the selection. We + * still want to inform the listeners that the selection has changed, but we + * don't want to send those changes "back to the client". + */ + private int ignoreSelectionClientSync = 0; + + private final GridHeader header = new GridHeader(this); + private final GridFooter footer = new GridFooter(this); + + private static final Method SELECTION_CHANGE_METHOD = ReflectTools + .findMethod(SelectionChangeListener.class, "selectionChange", + SelectionChangeEvent.class); + + private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools + .findMethod(SortOrderChangeListener.class, "sortOrderChange", + SortOrderChangeEvent.class); + + /** + * Creates a new Grid using the given datasource. + * + * @param datasource + * the data source for the grid + */ + public Grid(final Container.Indexed datasource) { + setContainerDataSource(datasource); + + setSelectionMode(SelectionMode.MULTI); + addSelectionChangeListener(new SelectionChangeListener() { + @Override + public void selectionChange(SelectionChangeEvent event) { + for (Object removedItemId : event.getRemoved()) { + getKeyMapper().unpin(removedItemId); + } + + for (Object addedItemId : event.getAdded()) { + if (!getKeyMapper().isPinned(addedItemId)) { + getKeyMapper().pin(addedItemId); + } + } + + List<String> keys = getKeyMapper().getKeys(getSelectedRows()); + + boolean markAsDirty = true; + + /* + * If this clause is true, it means that the selection event + * originated from the client. This means that we don't want to + * send the changes back to the client (markAsDirty => false). + */ + if (ignoreSelectionClientSync > 0) { + ignoreSelectionClientSync--; + markAsDirty = false; + + try { + + /* + * Make sure that the diffstate is aware of the + * "undirty" modification, so that the diffs are + * calculated correctly the next time we actually want + * to send the selection state to the client. + */ + getUI().getConnectorTracker().getDiffState(Grid.this) + .put("selectedKeys", new JSONArray(keys)); + } catch (JSONException e) { + throw new RuntimeException("Internal error", e); + } + } + + getState(markAsDirty).selectedKeys = keys; + } + }); + + registerRpc(new GridServerRpc() { + + @Override + public void selectionChange(List<String> selection) { + final HashSet<Object> newSelection = new HashSet<Object>( + getKeyMapper().getItemIds(selection)); + final HashSet<Object> oldSelection = new HashSet<Object>( + getSelectedRows()); + + SetView<Object> addedItemIds = Sets.difference(newSelection, + oldSelection); + SetView<Object> removedItemIds = Sets.difference(oldSelection, + newSelection); + + if (!removedItemIds.isEmpty()) { + /* + * Since these changes come from the client, we want to + * modify the selection model and get that event fired to + * all the listeners. One of the listeners is our internal + * selection listener, and this tells it not to send the + * selection event back to the client. + */ + ignoreSelectionClientSync++; + + if (removedItemIds.size() == 1) { + deselect(removedItemIds.iterator().next()); + } else { + assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi"; + ((SelectionModel.Multi) getSelectionModel()) + .deselect(removedItemIds); + } + } + + if (!addedItemIds.isEmpty()) { + /* + * Since these changes come from the client, we want to + * modify the selection model and get that event fired to + * all the listeners. One of the listeners is our internal + * selection listener, and this tells it not to send the + * selection event back to the client. + */ + ignoreSelectionClientSync++; + + if (addedItemIds.size() == 1) { + select(addedItemIds.iterator().next()); + } else { + assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi"; + ((SelectionModel.Multi) getSelectionModel()) + .select(addedItemIds); + } + } + } + + @Override + public void sort(String[] columnIds, SortDirection[] directions, + SortEventOriginator originator) { + assert columnIds.length == directions.length; + + List<SortOrder> order = new ArrayList<SortOrder>( + columnIds.length); + for (int i = 0; i < columnIds.length; i++) { + Object propertyId = getPropertyIdByColumnId(columnIds[i]); + order.add(new SortOrder(propertyId, directions[i])); + } + + setSortOrder(order, originator); + } + }); + } + + /** + * Sets the grid data source. + * + * @param container + * The container data source. Cannot be null. + * @throws IllegalArgumentException + * if the data source is null + */ + public void setContainerDataSource(Container.Indexed container) { + + if (container == null) { + throw new IllegalArgumentException( + "Cannot set the datasource to null"); + } + if (datasource == container) { + return; + } + + // Remove old listeners + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .removePropertySetChangeListener(propertyListener); + } + + if (datasourceExtension != null) { + removeExtension(datasourceExtension); + } + + datasource = container; + + // + // Adjust sort order + // + + if (container instanceof Container.Sortable) { + + // If the container is sortable, go through the current sort order + // and match each item to the sortable properties of the new + // container. If the new container does not support an item in the + // current sort order, that item is removed from the current sort + // order list. + Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource()) + .getSortableContainerPropertyIds(); + + Iterator<SortOrder> i = sortOrder.iterator(); + while (i.hasNext()) { + if (!sortableProps.contains(i.next().getPropertyId())) { + i.remove(); + } + } + + sort(SortEventOriginator.INTERNAL); + } else { + + // If the new container is not sortable, we'll just re-set the sort + // order altogether. + clearSortOrder(); + } + + datasourceExtension = new RpcDataProviderExtension(container); + datasourceExtension.extend(this); + + /* + * selectionModel == null when the invocation comes from the + * constructor. + */ + if (selectionModel != null) { + selectionModel.reset(); + } + + // Listen to changes in properties and remove columns if needed + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .addPropertySetChangeListener(propertyListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ + + getState().columns.clear(); + setLastFrozenPropertyId(null); + + // Add columns + HeaderRow row = getHeader().getDefaultRow(); + for (Object propertyId : datasource.getContainerPropertyIds()) { + if (!columns.containsKey(propertyId)) { + GridColumn column = appendColumn(propertyId); + + // Initial sorting is defined by container + if (datasource instanceof Sortable) { + column.setSortable(((Sortable) datasource) + .getSortableContainerPropertyIds().contains( + propertyId)); + } + + // Add by default property id as column header + row.getCell(propertyId).setText(String.valueOf(propertyId)); + } + } + } + + /** + * Returns the grid data source. + * + * @return the container data source of the grid + */ + public Container.Indexed getContainerDatasource() { + return datasource; + } + + /** + * Returns a column based on the property id + * + * @param propertyId + * the property id of the column + * @return the column or <code>null</code> if not found + */ + public GridColumn getColumn(Object propertyId) { + return columns.get(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a {@link GridColumn} by + * referencing its generated state id. Also used by {@link GridColumn} to + * verify if it has been detached from the {@link Grid}. + * + * @param columnId + * the client id generated for the column when the column is + * added to the grid + * @return the column with the id or <code>null</code> if not found + */ + GridColumn getColumnByColumnId(String columnId) { + Object propertyId = getPropertyIdByColumnId(columnId); + return getColumn(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a property id by referencing + * the columns generated state id. + * + * @param columnId + * The state id of the column + * @return The column instance or null if not found + */ + Object getPropertyIdByColumnId(String columnId) { + return columnKeys.get(columnId); + } + + @Override + protected GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected GridState getState(boolean markAsDirty) { + return (GridState) super.getState(markAsDirty); + } + + /** + * Creates a new column based on a property id and appends it as the last + * column. + * + * @param datasourcePropertyId + * The property id of a property in the datasource + */ + private GridColumn appendColumn(Object datasourcePropertyId) { + if (datasourcePropertyId == null) { + throw new IllegalArgumentException("Property id cannot be null"); + } + assert datasource.getContainerPropertyIds().contains( + datasourcePropertyId) : "Datasource should contain the property id"; + + GridColumnState columnState = new GridColumnState(); + columnState.id = columnKeys.key(datasourcePropertyId); + getState().columns.add(columnState); + + for (int i = 0; i < getHeader().getRowCount(); ++i) { + getHeader().getRow(i).addCell(datasourcePropertyId); + } + + for (int i = 0; i < getFooter().getRowCount(); ++i) { + getFooter().getRow(i).addCell(datasourcePropertyId); + } + + GridColumn column = new GridColumn(this, columnState); + columns.put(datasourcePropertyId, column); + + return column; + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or <code>null</code> to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + void setLastFrozenColumn(GridColumn lastFrozenColumn) { + /* + * TODO: If and when Grid supports column reordering or insertion of + * columns before other columns, make sure to mention that adding + * columns before lastFrozenColumn will change the frozen column count + */ + + if (lastFrozenColumn == null) { + getState().lastFrozenColumnId = null; + } else if (columns.containsValue(lastFrozenColumn)) { + getState().lastFrozenColumnId = lastFrozenColumn.getState().id; + } else { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the indicated property will be frozen in + * place when the grid is scrolled sideways. + * <p> + * <em>Note:</em> If the container used by this grid supports a propertyId + * <code>null</code>, it can never be defined as the last frozen column, as + * a <code>null</code> parameter will always reset the frozen columns in + * Grid. + * + * @param propertyId + * the property id corresponding to the column that should be the + * last frozen column, or <code>null</code> to not have any + * columns frozen. + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenPropertyId(Object propertyId) { + final GridColumn column; + if (propertyId == null) { + column = null; + } else { + column = getColumn(propertyId); + if (column == null) { + throw new IllegalArgumentException( + "property id does not exist."); + } + } + setLastFrozenColumn(column); + } + + /** + * Gets the rightmost frozen column in the grid. + * <p> + * <em>Note:</em> Most often, this method returns the very value set with + * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be + * reset to <code>null</code> if the column is detached from this grid. + * + * @return the rightmost frozen column in the grid, or <code>null</code> if + * no columns are frozen. + */ + public Object getLastFrozenPropertyId() { + return columnKeys.get(getState().lastFrozenColumnId); + } + + /** + * Scrolls to a certain item, using {@link ScrollDestination#ANY}. + * + * @param itemId + * id of item to scroll to. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollTo(Object itemId) throws IllegalArgumentException { + scrollTo(itemId, ScrollDestination.ANY); + } + + /** + * Scrolls to a certain item, using user-specified scroll destination. + * + * @param itemId + * id of item to scroll to. + * @param destination + * value specifying desired position of scrolled-to row. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollTo(Object itemId, ScrollDestination destination) + throws IllegalArgumentException { + + int row = datasource.indexOfId(itemId); + + if (row == -1) { + throw new IllegalArgumentException( + "Item with specified ID does not exist in data source"); + } + + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToRow(row, destination); + } + + /** + * Scrolls to the beginning of the first data row. + */ + public void scrollToStart() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToStart(); + } + + /** + * Scrolls to the end of the last data row. + */ + public void scrollToEnd() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToEnd(); + } + + /** + * Sets the number of rows that should be visible in Grid's body, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * If Grid is currently not in {@link HeightMode#ROW}, the given value is + * remembered, and applied once the mode is applied. + * + * @param rows + * The height in terms of number of rows displayed in Grid's + * body. If Grid doesn't contain enough rows, white space is + * displayed instead. If <code>null</code> is given, then Grid's + * height is undefined + * @throws IllegalArgumentException + * if {@code rows} is zero or less + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isInifinite(double) + * infinite} + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isNaN(double) NaN} + */ + public void setHeightByRows(double rows) { + if (rows <= 0.0d) { + throw new IllegalArgumentException( + "More than zero rows must be shown."); + } else if (Double.isInfinite(rows)) { + throw new IllegalArgumentException( + "Grid doesn't support infinite heights"); + } else if (Double.isNaN(rows)) { + throw new IllegalArgumentException("NaN is not a valid row count"); + } + + getState().heightByRows = rows; + } + + /** + * Gets the amount of rows in Grid's body that are shown, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * + * @return the amount of rows that are being shown in Grid's body + * @see #setHeightByRows(double) + */ + public double getHeightByRows() { + return getState(false).heightByRows; + } + + /** + * {@inheritDoc} + * <p> + * <em>Note:</em> This method will change the widget's size in the browser + * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. + * + * @see #setHeightMode(HeightMode) + */ + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + } + + /** + * Defines the mode in which the Grid widget's height is calculated. + * <p> + * If {@link HeightMode#CSS} is given, Grid will respect the values given + * via a {@code setHeight}-method, and behave as a traditional Component. + * <p> + * If {@link HeightMode#ROW} is given, Grid will make sure that the body + * will display as many rows as {@link #getHeightByRows()} defines. + * <em>Note:</em> If headers/footers are inserted or removed, the widget + * will resize itself to still display the required amount of rows in its + * body. It also takes the horizontal scrollbar into account. + * + * @param heightMode + * the mode in to which Grid should be set + */ + public void setHeightMode(HeightMode heightMode) { + /* + * This method is a workaround for the fact that Vaadin re-applies + * widget dimensions (height/width) on each state change event. The + * original design was to have setHeight an setHeightByRow be equals, + * and whichever was called the latest was considered in effect. + * + * But, because of Vaadin always calling setHeight on the widget, this + * approach doesn't work. + */ + + getState().heightMode = heightMode; + } + + /** + * Returns the current {@link HeightMode} the Grid is in. + * <p> + * Defaults to {@link HeightMode#CSS}. + * + * @return the current HeightMode + */ + public HeightMode getHeightMode() { + return getState(false).heightMode; + } + + /* Selection related methods: */ + + /** + * Takes a new {@link SelectionModel} into use. + * <p> + * The SelectionModel that is previously in use will have all its items + * deselected. + * <p> + * If the given SelectionModel is already in use, this method does nothing. + * + * @param selectionModel + * the new SelectionModel to use + * @throws IllegalArgumentException + * if {@code selectionModel} is <code>null</code> + */ + public void setSelectionModel(SelectionModel selectionModel) + throws IllegalArgumentException { + if (selectionModel == null) { + throw new IllegalArgumentException( + "Selection model may not be null"); + } + + if (this.selectionModel != selectionModel) { + // this.selectionModel is null on init + if (this.selectionModel != null) { + this.selectionModel.reset(); + this.selectionModel.setGrid(null); + } + + this.selectionModel = selectionModel; + this.selectionModel.setGrid(this); + this.selectionModel.reset(); + + if (selectionModel.getClass().equals(SingleSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.SINGLE; + } else if (selectionModel.getClass().equals( + MultiSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.MULTI; + } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.NONE; + } else { + throw new UnsupportedOperationException("Grid currently " + + "supports only its own bundled selection models"); + } + } + } + + /** + * Returns the currently used {@link SelectionModel}. + * + * @return the currently used SelectionModel + */ + public SelectionModel getSelectionModel() { + return selectionModel; + } + + /** + * Changes the Grid's selection mode. + * <p> + * Grid supports three selection modes: multiselect, single select and no + * selection, and this is a conveniency method for choosing between one of + * them. + * <P> + * Technically, this method is a shortcut that can be used instead of + * calling {@code setSelectionModel} with a specific SelectionModel + * instance. Grid comes with three built-in SelectionModel classes, and the + * {@link SelectionMode} enum represents each of them. + * <p> + * Essentially, the two following method calls are equivalent: + * <p> + * <code><pre> + * grid.setSelectionMode(SelectionMode.MULTI); + * grid.setSelectionModel(new MultiSelectionMode()); + * </pre></code> + * + * + * @param selectionMode + * the selection mode to switch to + * @return The {@link SelectionModel} instance that was taken into use + * @throws IllegalArgumentException + * if {@code selectionMode} is <code>null</code> + * @see SelectionModel + */ + public SelectionModel setSelectionMode(final SelectionMode selectionMode) + throws IllegalArgumentException { + if (selectionMode == null) { + throw new IllegalArgumentException("selection mode may not be null"); + } + final SelectionModel newSelectionModel = selectionMode.createModel(); + setSelectionModel(newSelectionModel); + return newSelectionModel; + } + + /** + * Checks whether an item is selected or not. + * + * @param itemId + * the item id to check for + * @return <code>true</code> iff the item is selected + */ + // keep this javadoc in sync with SelectionModel.isSelected + public boolean isSelected(Object itemId) { + return selectionModel.isSelected(itemId); + } + + /** + * Returns a collection of all the currently selected itemIds. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. + * + * @return a collection of all the currently selected itemIds + */ + // keep this javadoc in sync with SelectionModel.getSelectedRows + public Collection<Object> getSelectedRows() { + return getSelectionModel().getSelectedRows(); + } + + /** + * Gets the item id of the currently selected item. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} is supported. + * + * @return the item id of the currently selected item, or <code>null</code> + * if nothing is selected + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} is not an instance of + * {@link SelectionModel.Single} + */ + // keep this javadoc in sync with SelectionModel.Single.getSelectedRow + public Object getSelectedRow() throws IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).getSelectedRow(); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'getSelectedRow' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")"); + } + } + + /** + * Marks an item as selected. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are + * supported. + * + * + * @param itemIds + * the itemId to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalArgumentException + * if the {@code itemId} doesn't exist in the currently active + * Container + * @throws IllegalStateException + * if the selection was illegal. One such reason might be that + * the implementation already had an item selected, and that + * needs to be explicitly deselected before re-selecting + * something + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} does not implement + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + // keep this javadoc in sync with SelectionModel.Single.select + public boolean select(Object itemId) throws IllegalArgumentException, + IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).select(itemId); + } else if (selectionModel instanceof SelectionModel.Multi) { + return ((SelectionModel.Multi) selectionModel).select(itemId); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'select' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Marks an item as deselected. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are + * supported. + * + * @param itemId + * the itemId to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalArgumentException + * if the {@code itemId} doesn't exist in the currently active + * Container + * @throws IllegalStateException + * if the deselection was illegal. One such reason might be that + * the implementation already had an item selected, and that + * needs to be explicitly deselected before re-selecting + * something + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} does not implement + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + // keep this javadoc in sync with SelectionModel.Single.deselect + public boolean deselect(Object itemId) throws IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).deselect(itemId); + } else if (selectionModel instanceof SelectionModel.Multi) { + return ((SelectionModel.Multi) selectionModel).deselect(itemId); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'deselect' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Fires a selection change event. + * <p> + * <strong>Note:</strong> This is not a method that should be called by + * application logic. This method is publicly accessible only so that + * {@link SelectionModel SelectionModels} would be able to inform Grid of + * these events. + * + * @param addedSelections + * the selections that were added by this event + * @param removedSelections + * the selections that were removed by this event + */ + public void fireSelectionChangeEvent(Collection<Object> oldSelection, + Collection<Object> newSelection) { + fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection)); + } + + @Override + public void addSelectionChangeListener(SelectionChangeListener listener) { + addListener(SelectionChangeEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + @Override + public void removeSelectionChangeListener(SelectionChangeListener listener) { + removeListener(SelectionChangeEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + /** + * Gets the + * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper + * DataProviderKeyMapper} being used by the data source. + * + * @return the key mapper being used by the data source + */ + DataProviderKeyMapper getKeyMapper() { + return datasourceExtension.getKeyMapper(); + } + + /** + * Adds a renderer to this grid's connector hierarchy. + * + * @param renderer + * the renderer to add + */ + void addRenderer(Renderer<?> renderer) { + addExtension(renderer); + } + + /** + * Sets the current sort order using the fluid Sort API. Read the + * documentation for {@link Sort} for more information. + * + * @param s + * a sort instance + */ + public void sort(Sort s) { + setSortOrder(s.build()); + } + + /** + * Sort this Grid in ascending order by a specified property. + * + * @param propertyId + * a property ID + */ + public void sort(Object propertyId) { + sort(propertyId, SortDirection.ASCENDING); + } + + /** + * Sort this Grid in user-specified {@link SortOrder} by a property. + * + * @param propertyId + * a property ID + * @param direction + * a sort order value (ascending/descending) + */ + public void sort(Object propertyId, SortDirection direction) { + sort(Sort.by(propertyId, direction)); + } + + /** + * Clear the current sort order, and re-sort the grid. + */ + public void clearSortOrder() { + sortOrder.clear(); + sort(false); + } + + /** + * Sets the sort order to use. This method throws + * {@link IllegalStateException} if the attached container is not a + * {@link Container.Sortable}, and {@link IllegalArgumentException} if a + * property in the list is not recognized by the container, or if the + * 'order' parameter is null. + * + * @param order + * a sort order list. + */ + public void setSortOrder(List<SortOrder> order) { + setSortOrder(order, SortEventOriginator.API); + } + + private void setSortOrder(List<SortOrder> order, + SortEventOriginator originator) { + if (!(getContainerDatasource() instanceof Container.Sortable)) { + throw new IllegalStateException( + "Attached container is not sortable (does not implement Container.Sortable)"); + } + + if (order == null) { + throw new IllegalArgumentException("Order list may not be null!"); + } + + sortOrder.clear(); + + Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource()) + .getSortableContainerPropertyIds(); + + for (SortOrder o : order) { + if (!sortableProps.contains(o.getPropertyId())) { + throw new IllegalArgumentException( + "Property " + + o.getPropertyId() + + " does not exist or is not sortable in the current container"); + } + } + + sortOrder.addAll(order); + sort(originator); + } + + /** + * Get the current sort order list. + * + * @return a sort order list + */ + public List<SortOrder> getSortOrder() { + return Collections.unmodifiableList(sortOrder); + } + + /** + * Apply sorting to data source. + */ + private void sort(SortEventOriginator originator) { + + Container c = getContainerDatasource(); + if (c instanceof Container.Sortable) { + Container.Sortable cs = (Container.Sortable) c; + + final int items = sortOrder.size(); + Object[] propertyIds = new Object[items]; + boolean[] directions = new boolean[items]; + + String[] columnKeys = new String[items]; + SortDirection[] stateDirs = new SortDirection[items]; + + for (int i = 0; i < items; ++i) { + SortOrder order = sortOrder.get(i); + + columnKeys[i] = this.columnKeys.key(order.getPropertyId()); + stateDirs[i] = order.getDirection(); + + propertyIds[i] = order.getPropertyId(); + switch (order.getDirection()) { + case ASCENDING: + directions[i] = true; + break; + case DESCENDING: + directions[i] = false; + break; + default: + throw new IllegalArgumentException("getDirection() of " + + order + " returned an unexpected value"); + } + } + + cs.sort(propertyIds, directions); + + fireEvent(new SortOrderChangeEvent(this, new ArrayList<SortOrder>( + sortOrder), originator)); + + getState().sortColumns = columnKeys; + getState(false).sortDirs = stateDirs; + } else { + throw new IllegalStateException( + "Container is not sortable (does not implement Container.Sortable)"); + } + } + + /** + * Adds a sort order change listener that gets notified when the sort order + * changes. + * + * @param listener + * the sort order change listener to add + */ + public void addSortOrderChangeListener(SortOrderChangeListener listener) { + addListener(SortOrderChangeEvent.class, listener, + SORT_ORDER_CHANGE_METHOD); + } + + /** + * Removes a sort order change listener previously added using + * {@link #addSortOrderChangeListener(SortOrderChangeListener)}. + * + * @param listener + * the sort order change listener to remove + */ + public void removeSortOrderChangeListener(SortOrderChangeListener listener) { + removeListener(SortOrderChangeEvent.class, listener, + SORT_ORDER_CHANGE_METHOD); + } + + /** + * Returns the header section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the header + */ + public GridHeader getHeader() { + return header; + } + + /** + * Returns the footer section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the footer + */ + public GridFooter getFooter() { + return footer; + } + + @Override + public Iterator<Component> iterator() { + List<Component> componentList = new ArrayList<Component>(); + + GridHeader header = getHeader(); + for (int i = 0; i < header.getRowCount(); ++i) { + HeaderRow row = header.getRow(i); + for (Object propId : datasource.getContainerPropertyIds()) { + HeaderCell cell = row.getCell(propId); + if (cell.getCellState().type == GridStaticCellType.WIDGET) { + componentList.add(cell.getComponent()); + } + } + } + + GridFooter footer = getFooter(); + for (int i = 0; i < footer.getRowCount(); ++i) { + FooterRow row = footer.getRow(i); + for (Object propId : datasource.getContainerPropertyIds()) { + FooterCell cell = row.getCell(propId); + if (cell.getCellState().type == GridStaticCellType.WIDGET) { + componentList.add(cell.getComponent()); + } + } + } + + return componentList.iterator(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java new file mode 100644 index 0000000000..0ef805eb2e --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -0,0 +1,427 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui.components.grid; + +import java.io.Serializable; + +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; +import com.vaadin.ui.UI; +import com.vaadin.ui.components.grid.renderers.TextRenderer; + +/** + * A column in the grid. Can be obtained by calling + * {@link Grid#getColumn(Object propertyId)}. + * + * @since + * @author Vaadin Ltd + */ +public class GridColumn implements Serializable { + + /** + * The state of the column shared to the client + */ + private final GridColumnState state; + + /** + * The grid this column is associated with + */ + private final Grid grid; + + private Converter<?, Object> converter; + + /** + * A check for allowing the {@link #GridColumn(Grid, GridColumnState) + * constructor} to call {@link #setConverter(Converter)} with a + * <code>null</code>, even if model and renderer aren't compatible. + */ + private boolean isFirstConverterAssignment = true; + + /** + * Internally used constructor. + * + * @param grid + * The grid this column belongs to. Should not be null. + * @param state + * the shared state of this column + */ + GridColumn(Grid grid, GridColumnState state) { + this.grid = grid; + this.state = state; + internalSetRenderer(new TextRenderer()); + } + + /** + * Returns the serializable state of this column that is sent to the client + * side connector. + * + * @return the internal state of the column + */ + GridColumnState getState() { + return state; + } + + /** + * Returns the caption of the header. By default the header caption is the + * property id of the column. + * + * @return the text in the default row of header, null if no default row + * + * @throws IllegalStateException + * if the column no longer is attached to the grid + */ + @Deprecated + public String getHeaderCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.header; + } + + /** + * Sets the caption of the header. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + @Deprecated + public void setHeaderCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.header = caption; + } + + /** + * Returns the caption of the footer. By default the captions are + * <code>null</code>. + * + * @return the text in the footer + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + @Deprecated + public String getFooterCaption() throws IllegalStateException { + checkColumnIsAttached(); + return getFooterCellState().text; + } + + /** + * Sets the caption of the footer. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + @Deprecated + public void setFooterCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + getFooterCellState().text = caption; + state.footer = caption; + grid.markAsDirty(); + } + + private CellState getFooterCellState() { + int index = grid.getState().columns.indexOf(state); + return grid.getState().footer.rows.get(0).cells.get(index); + } + + /** + * Returns the width (in pixels). By default a column is 100px wide. + * + * @return the width in pixels of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public int getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return state.width; + } + + /** + * Sets the width (in pixels). + * + * @param pixelWidth + * the new pixel width of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero + */ + public void setWidth(int pixelWidth) throws IllegalStateException, + IllegalArgumentException { + checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0"); + } + state.width = pixelWidth; + grid.markAsDirty(); + } + + /** + * Marks the column width as undefined meaning that the grid is free to + * resize the column based on the cell contents and available space in the + * grid. + */ + public void setWidthUndefined() { + checkColumnIsAttached(); + state.width = -1; + grid.markAsDirty(); + } + + /** + * Is this column visible in the grid. By default all columns are visible. + * + * @return <code>true</code> if the column is visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public boolean isVisible() throws IllegalStateException { + checkColumnIsAttached(); + return state.visible; + } + + /** + * Set the visibility of this column + * + * @param visible + * is the column visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setVisible(boolean visible) throws IllegalStateException { + checkColumnIsAttached(); + state.visible = visible; + grid.markAsDirty(); + } + + /** + * Checks if column is attached and throws an {@link IllegalStateException} + * if it is not + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkColumnIsAttached() throws IllegalStateException { + if (grid.getColumnByColumnId(state.id) == null) { + throw new IllegalStateException("Column no longer exists."); + } + } + + /** + * Sets this column as the last frozen column in its grid. + * + * @throws IllegalArgumentException + * if the column is no longer attached to any grid + * @see Grid#setLastFrozenColumn(GridColumn) + */ + public void setLastFrozenColumn() { + checkColumnIsAttached(); + grid.setLastFrozenColumn(this); + } + + /** + * Sets the renderer for this column. + * <p> + * If a suitable converter isn't defined explicitly, the session converter + * factory is used to find a compatible converter. + * + * @param renderer + * the renderer to use + * @throws IllegalArgumentException + * if no compatible converter could be found + * + * @see VaadinSession#getConverterFactory() + * @see ConverterUtil#getConverter(Class, Class, VaadinSession) + * @see #setConverter(Converter) + */ + public void setRenderer(Renderer<?> renderer) { + if (!internalSetRenderer(renderer)) { + throw new IllegalArgumentException( + "Could not find a converter for converting from the model type " + + getModelType() + + " to the renderer presentation type " + + renderer.getPresentationType()); + } + } + + /** + * Sets the renderer for this column and the converter used to convert from + * the property value type to the renderer presentation type. + * + * @param renderer + * the renderer to use, cannot be null + * @param converter + * the converter to use + * + * @throws IllegalArgumentException + * if the renderer is already associated with a grid column + */ + public <T> void setRenderer(Renderer<T> renderer, + Converter<? extends T, ?> converter) { + if (renderer.getParent() != null) { + throw new IllegalArgumentException( + "Cannot set a renderer that is already connected to a grid column"); + } + + if (getRenderer() != null) { + grid.removeExtension(getRenderer()); + } + + grid.addRenderer(renderer); + state.rendererConnector = renderer; + setConverter(converter); + } + + /** + * Sets the converter used to convert from the property value type to the + * renderer presentation type. + * + * @param converter + * the converter to use, or {@code null} to not use any + * converters + * @throws IllegalArgumentException + * if the types are not compatible + */ + public void setConverter(Converter<?, ?> converter) + throws IllegalArgumentException { + Class<?> modelType = getModelType(); + if (converter != null) { + if (!converter.getModelType().isAssignableFrom(modelType)) { + throw new IllegalArgumentException("The converter model type " + + converter.getModelType() + + " is not compatible with the property type " + + modelType); + + } else if (!getRenderer().getPresentationType().isAssignableFrom( + converter.getPresentationType())) { + throw new IllegalArgumentException( + "The converter presentation type " + + converter.getPresentationType() + + " is not compatible with the renderer presentation type " + + getRenderer().getPresentationType()); + } + } + + else { + /* + * Since the converter is null (i.e. will be removed), we need to + * know that the renderer and model are compatible. If not, we can't + * allow for this to happen. + * + * The constructor is allowed to call this method with null without + * any compatibility checks, therefore we have a special case for + * it. + */ + + Class<?> rendererPresentationType = getRenderer() + .getPresentationType(); + if (!isFirstConverterAssignment + && !rendererPresentationType.isAssignableFrom(modelType)) { + throw new IllegalArgumentException("Cannot remove converter, " + + "as renderer's presentation type " + + rendererPresentationType.getName() + " and column's " + + "model " + modelType.getName() + " type aren't " + + "directly with each other"); + } + } + + isFirstConverterAssignment = false; + + @SuppressWarnings("unchecked") + Converter<?, Object> castConverter = (Converter<?, Object>) converter; + this.converter = castConverter; + } + + /** + * Returns the renderer instance used by this column. + * + * @return the renderer + */ + public Renderer<?> getRenderer() { + return (Renderer<?>) getState().rendererConnector; + } + + /** + * Returns the converter instance used by this column. + * + * @return the converter + */ + public Converter<?, ?> getConverter() { + return converter; + } + + private <T> boolean internalSetRenderer(Renderer<T> renderer) { + + Converter<? extends T, ?> converter; + if (isCompatibleWithProperty(renderer, getConverter())) { + // Use the existing converter (possibly none) if types compatible + converter = (Converter<? extends T, ?>) getConverter(); + } else { + converter = ConverterUtil.getConverter( + renderer.getPresentationType(), getModelType(), + getSession()); + } + setRenderer(renderer, converter); + return isCompatibleWithProperty(renderer, converter); + } + + private VaadinSession getSession() { + UI ui = grid.getUI(); + return ui != null ? ui.getSession() : null; + } + + private boolean isCompatibleWithProperty(Renderer<?> renderer, + Converter<?, ?> converter) { + Class<?> type; + if (converter == null) { + type = getModelType(); + } else { + type = converter.getPresentationType(); + } + return renderer.getPresentationType().isAssignableFrom(type); + } + + private Class<?> getModelType() { + return grid.getContainerDatasource().getType( + grid.getPropertyIdByColumnId(state.id)); + } + + /** + * Should sorting controls be available for the column + * + * @param sortable + * <code>true</code> if the sorting controls should be visible. + */ + public void setSortable(boolean sortable) { + checkColumnIsAttached(); + state.sortable = sortable; + grid.markAsDirty(); + } + + /** + * Are the sorting controls visible in the column header + */ + public boolean isSortable() { + return state.sortable; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridFooter.java b/server/src/com/vaadin/ui/components/grid/GridFooter.java new file mode 100644 index 0000000000..0a28a481cf --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridFooter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.shared.ui.grid.GridStaticSectionState; + +/** + * Represents the footer section of a Grid. By default Footer is not visible. + * + * @since + * @author Vaadin Ltd + */ +public class GridFooter extends GridStaticSection<GridFooter.FooterRow> { + + public class FooterRow extends GridStaticSection.StaticRow<FooterCell> { + + protected FooterRow(GridStaticSection<?> section) { + super(section); + } + + @Override + protected FooterCell createCell() { + return new FooterCell(this); + } + + } + + public class FooterCell extends GridStaticSection.StaticCell { + + protected FooterCell(FooterRow row) { + super(row); + } + } + + private final GridStaticSectionState footerState = new GridStaticSectionState(); + + protected GridFooter(Grid grid) { + this.grid = grid; + grid.getState(true).footer = footerState; + setVisible(false); + } + + @Override + protected GridStaticSectionState getSectionState() { + return footerState; + } + + @Override + protected FooterRow createRow() { + return new FooterRow(this); + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/GridHeader.java b/server/src/com/vaadin/ui/components/grid/GridHeader.java new file mode 100644 index 0000000000..9d7ec24a97 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridHeader.java @@ -0,0 +1,124 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.shared.ui.grid.GridStaticSectionState; + +/** + * Represents the header section of a Grid. + * + * @since + * @author Vaadin Ltd + */ +public class GridHeader extends GridStaticSection<GridHeader.HeaderRow> { + + public class HeaderRow extends GridStaticSection.StaticRow<HeaderCell> { + + protected HeaderRow(GridStaticSection<?> section) { + super(section); + } + + private void setDefaultRow(boolean value) { + getRowState().defaultRow = value; + } + + @Override + protected HeaderCell createCell() { + return new HeaderCell(this); + } + } + + public class HeaderCell extends GridStaticSection.StaticCell { + + protected HeaderCell(HeaderRow row) { + super(row); + } + } + + private HeaderRow defaultRow = null; + private final GridStaticSectionState headerState = new GridStaticSectionState(); + + protected GridHeader(Grid grid) { + this.grid = grid; + grid.getState(true).header = headerState; + HeaderRow row = createRow(); + rows.add(row); + setDefaultRow(row); + getSectionState().rows.add(row.getRowState()); + } + + /** + * Sets the default row of this header. The default row is a special header + * row providing a user interface for sorting columns. + * + * @param row + * the new default row, or null for no default row + * + * @throws IllegalArgumentException + * this header does not contain the row + */ + public void setDefaultRow(HeaderRow row) { + if (row == defaultRow) { + return; + } + + if (row != null && !rows.contains(row)) { + throw new IllegalArgumentException( + "Cannot set a default row that does not exist in the section"); + } + + if (defaultRow != null) { + defaultRow.setDefaultRow(false); + } + + if (row != null) { + row.setDefaultRow(true); + } + + defaultRow = row; + markAsDirty(); + } + + /** + * Returns the current default row of this header. The default row is a + * special header row providing a user interface for sorting columns. + * + * @return the default row or null if no default row set + */ + public HeaderRow getDefaultRow() { + return defaultRow; + } + + @Override + protected GridStaticSectionState getSectionState() { + return headerState; + } + + @Override + protected HeaderRow createRow() { + return new HeaderRow(this); + } + + @Override + public HeaderRow removeRow(int rowIndex) { + HeaderRow row = super.removeRow(rowIndex); + if (row == defaultRow) { + // Default Header Row was just removed. + setDefaultRow(null); + } + return row; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridStaticSection.java b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java new file mode 100644 index 0000000000..eb098d0d4e --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java @@ -0,0 +1,425 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.GridStaticSectionState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; +import com.vaadin.ui.Component; + +/** + * Abstract base class for Grid header and footer sections. + * + * @since + * @author Vaadin Ltd + * @param <ROWTYPE> + * the type of the rows in the section + */ +abstract class GridStaticSection<ROWTYPE extends GridStaticSection.StaticRow<?>> + implements Serializable { + + /** + * Abstract base class for Grid header and footer rows. + * + * @param <CELLTYPE> + * the type of the cells in the row + */ + abstract static class StaticRow<CELLTYPE extends StaticCell> implements + Serializable { + + private RowState rowState = new RowState(); + protected GridStaticSection<?> section; + private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>(); + private Collection<List<CELLTYPE>> cellGroups = new HashSet<List<CELLTYPE>>(); + + protected StaticRow(GridStaticSection<?> section) { + this.section = section; + } + + protected void addCell(Object propertyId) { + CELLTYPE cell = createCell(); + cells.put(propertyId, cell); + rowState.cells.add(cell.getCellState()); + } + + /** + * Creates and returns a new instance of the cell type. + * + * @return the created cell + */ + protected abstract CELLTYPE createCell(); + + protected RowState getRowState() { + return rowState; + } + + /** + * Returns the cell at the given position in this row. + * + * @param propertyId + * the itemId of column + * @return the cell on given column + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public CELLTYPE getCell(Object propertyId) { + return cells.get(propertyId); + } + + /** + * Merges cells in a row + * + * @param cells + * The cells to be merged + * @return The first cell of the merged cells + */ + protected CELLTYPE join(List<CELLTYPE> cells) { + assert cells.size() > 1 : "You cannot merge less than 2 cells together"; + + // Ensure no cell is already grouped + for (CELLTYPE cell : cells) { + if (getCellGroupForCell(cell) != null) { + throw new IllegalStateException("Cell " + cell.getText() + + " is already grouped."); + } + } + + // Ensure continuous range + Iterator<CELLTYPE> cellIterator = this.cells.values().iterator(); + CELLTYPE current = null; + int firstIndex = 0; + + while (cellIterator.hasNext()) { + current = cellIterator.next(); + if (current == cells.get(0)) { + break; + } + firstIndex++; + } + + for (int i = 1; i < cells.size(); ++i) { + current = cellIterator.next(); + + if (current != cells.get(i)) { + throw new IllegalStateException( + "Cell range must be a continous range"); + } + } + + // Create a new group + final ArrayList<CELLTYPE> cellGroup = new ArrayList<CELLTYPE>(cells); + cellGroups.add(cellGroup); + + // Add group to state + List<Integer> stateGroup = new ArrayList<Integer>(); + for (int i = 0; i < cells.size(); ++i) { + stateGroup.add(firstIndex + i); + } + rowState.cellGroups.add(stateGroup); + section.markAsDirty(); + + // Returns first cell of group + return cells.get(0); + } + + /** + * Merges columns cells in a row + * + * @param properties + * The column properties which header should be merged + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(Object... properties) { + List<CELLTYPE> cells = new ArrayList<CELLTYPE>(); + for (int i = 0; i < properties.length; ++i) { + cells.add(getCell(properties[i])); + } + + return join(cells); + } + + /** + * Merges columns cells in a row + * + * @param cells + * The cells to merge. Must be from the same row. + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(CELLTYPE... cells) { + return join(Arrays.asList(cells)); + } + + private List<CELLTYPE> getCellGroupForCell(CELLTYPE cell) { + for (List<CELLTYPE> group : cellGroups) { + if (group.contains(cell)) { + return group; + } + } + return null; + } + } + + /** + * A header or footer cell. Has a simple textual caption. + */ + abstract static class StaticCell implements Serializable { + + private CellState cellState = new CellState(); + private StaticRow<?> row; + + protected StaticCell(StaticRow<?> row) { + this.row = row; + } + + /** + * Gets the row where this cell is. + * + * @return row for this cell + */ + public StaticRow<?> getRow() { + return row; + } + + protected CellState getCellState() { + return cellState; + } + + /** + * Sets the text displayed in this cell. + * + * @param text + * a plain text caption + */ + public void setText(String text) { + cellState.text = text; + cellState.type = GridStaticCellType.TEXT; + row.section.markAsDirty(); + } + + /** + * Returns the text displayed in this cell. + * + * @return the plain text caption + */ + public String getText() { + if (cellState.type != GridStaticCellType.TEXT) { + throw new IllegalStateException( + "Cannot fetch Text from a cell with type " + + cellState.type); + } + return cellState.text; + } + + /** + * Returns the HTML content displayed in this cell. + * + * @return the html + * + */ + public String getHtml() { + if (cellState.type != GridStaticCellType.HTML) { + throw new IllegalStateException( + "Cannot fetch HTML from a cell with type " + + cellState.type); + } + return cellState.html; + } + + /** + * Sets the HTML content displayed in this cell. + * + * @param html + * the html to set + */ + public void setHtml(String html) { + cellState.html = html; + cellState.type = GridStaticCellType.HTML; + row.section.markAsDirty(); + } + + /** + * Returns the component displayed in this cell. + * + * @return the component + */ + public Component getComponent() { + if (cellState.type != GridStaticCellType.WIDGET) { + throw new IllegalStateException( + "Cannot fetch Component from a cell with type " + + cellState.type); + } + return (Component) cellState.connector; + } + + /** + * Sets the component displayed in this cell. + * + * @param component + * the component to set + */ + public void setComponent(Component component) { + component.setParent(row.section.grid); + cellState.connector = component; + cellState.type = GridStaticCellType.WIDGET; + row.section.markAsDirty(); + } + } + + protected Grid grid; + protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>(); + + /** + * Sets the visibility of the whole section. + * + * @param visible + * true to show this section, false to hide + */ + public void setVisible(boolean visible) { + if (getSectionState().visible != visible) { + getSectionState().visible = visible; + markAsDirty(); + } + } + + /** + * Returns the visibility of this section. + * + * @return true if visible, false otherwise. + */ + public boolean isVisible() { + return getSectionState().visible; + } + + /** + * Removes the row at the given position. + * + * @param index + * the position of the row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public ROWTYPE removeRow(int rowIndex) { + ROWTYPE row = rows.remove(rowIndex); + getSectionState().rows.remove(rowIndex); + + markAsDirty(); + return row; + } + + /** + * Removes the given row from the section. + * + * @param row + * the row to be removed + * + * @throws IllegalArgumentException + * if the row does not exist in this section + */ + public void removeRow(ROWTYPE row) { + try { + removeRow(rows.indexOf(row)); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "Section does not contain the given row"); + } + } + + /** + * Gets row at given index. + * + * @param rowIndex + * 0 based index for row. Counted from top to bottom + * @return row at given index + */ + public ROWTYPE getRow(int rowIndex) { + return rows.get(rowIndex); + } + + /** + * Adds a new row at the top of this section. + * + * @return the new row + */ + public ROWTYPE prependRow() { + return addRowAt(0); + } + + /** + * Adds a new row at the bottom of this section. + * + * @return the new row + */ + public ROWTYPE appendRow() { + return addRowAt(rows.size()); + } + + /** + * Inserts a new row at the given position. + * + * @param index + * the position at which to insert the row + * @return the new row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + */ + public ROWTYPE addRowAt(int index) { + ROWTYPE row = createRow(); + rows.add(index, row); + getSectionState().rows.add(index, row.getRowState()); + + Indexed dataSource = grid.getContainerDatasource(); + for (Object id : dataSource.getContainerPropertyIds()) { + row.addCell(id); + } + + markAsDirty(); + return row; + } + + /** + * Gets the amount of rows in this section. + * + * @return row count + */ + public int getRowCount() { + return rows.size(); + } + + protected abstract GridStaticSectionState getSectionState(); + + protected abstract ROWTYPE createRow(); + + /** + * Informs the grid that state has changed and it should be redrawn. + */ + protected void markAsDirty() { + grid.markAsDirty(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/Renderer.java b/server/src/com/vaadin/ui/components/grid/Renderer.java new file mode 100644 index 0000000000..b9074fb9f7 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/Renderer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.Extension; + +/** + * A ClientConnector for controlling client-side + * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. Renderers + * currently extend the Extension interface, but this fact should be regarded as + * an implementation detail and subject to change in a future major or minor + * Vaadin revision. + * + * @param <T> + * the type this renderer knows how to present + * + * @since + * @author Vaadin Ltd + */ +public interface Renderer<T> extends Extension { + + /** + * Returns the class literal corresponding to the presentation type T. + * + * @return the class literal of T + */ + Class<T> getPresentationType(); + + /** + * Encodes the given value into a form that can be transferred to the + * client. The type of the returned value must be one of the types that are + * accepted by <a href= + * "http://www.json.org/javadoc/org/json/JSONObject.html#put%28java.lang.String,%20java.lang.Object%29" + * >{@code org.json.JSONObject#put(String, Object)}</a>. + * + * @param value + * the value to encode + * @return an encoded form of the given value + */ + Object encode(T value); + + /** + * This method is inherited from Extension but should never be called + * directly with a Renderer. + */ + @Override + @Deprecated + void remove(); + + /** + * This method is inherited from Extension but should never be called + * directly with a Renderer. + */ + @Override + @Deprecated + void setParent(ClientConnector parent); +} diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java new file mode 100644 index 0000000000..690fcdf1c4 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import java.util.List; + +import com.vaadin.shared.ui.grid.SortEventOriginator; +import com.vaadin.ui.Component; +import com.vaadin.ui.components.grid.sort.SortOrder; + +/** + * Event fired by {@link Grid} when the sort order has changed. + * + * @see SortOrderChangeListener + * + * @since + * @author Vaadin Ltd + */ +public class SortOrderChangeEvent extends Component.Event { + + private final List<SortOrder> sortOrder; + private final SortEventOriginator originator; + + /** + * Creates a new sort order change event for a grid and a sort order list. + * + * @param grid + * the grid from which the event originates + * @param sortOrder + * the new sort order list + * @param wasInitiatedByUser + * should be set to true if this event results from end-user + * interaction instead of an API call or side effect + */ + public SortOrderChangeEvent(Grid grid, List<SortOrder> sortOrder, + SortEventOriginator originator) { + super(grid); + this.sortOrder = sortOrder; + this.originator = originator; + } + + /** + * Gets the sort order list. + * + * @return the sort order list + */ + public List<SortOrder> getSortOrder() { + return sortOrder; + } + + /** + * Gets a value describing the originator of this event, i.e. what actions + * resulted in this event being fired. + * + * @return a sort event originator value + * + * @see SortEventOriginator + */ + public SortEventOriginator getOriginator() { + return originator; + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java new file mode 100644 index 0000000000..82d7ba3108 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid; + +import java.io.Serializable; + +/** + * Listener for sort order change events from {@link Grid}. + * + * @since + * @author Vaadin Ltd + */ +public interface SortOrderChangeListener extends Serializable { + /** + * Called when the sort order has changed. + * + * @param event + * the sort order change event + */ + public void sortOrderChange(SortOrderChangeEvent event); +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java new file mode 100644 index 0000000000..736b61d9e2 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java @@ -0,0 +1,152 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import java.text.DateFormat; +import java.util.Date; +import java.util.Locale; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting date values. + * + * @since + * @author Vaadin Ltd + */ +public class DateRenderer extends AbstractRenderer<Date> { + private final Locale locale; + private final String formatString; + private final DateFormat dateFormat; + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the {@link Date#toString()} + * representation for the default locale. + */ + public DateRenderer() { + this(Locale.getDefault()); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the {@link Date#toString()} + * representation for the given locale. + * + * @param locale + * the locale in which to present dates + * @throws IllegalArgumentException + * if {@code locale} is {@code null} + */ + public DateRenderer(Locale locale) throws IllegalArgumentException { + this("%s", locale); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the given string format, as + * displayed in the default locale. + * + * @param formatString + * the format string with which to format the date + * @throws IllegalArgumentException + * if {@code formatString} is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public DateRenderer(String formatString) throws IllegalArgumentException { + this(formatString, Locale.getDefault()); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the given string format, as + * displayed in the given locale. + * + * @param formatString + * the format string to format the date with + * @param locale + * the locale to use + * @throws IllegalArgumentException + * if either argument is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public DateRenderer(String formatString, Locale locale) + throws IllegalArgumentException { + super(Date.class); + + if (formatString == null) { + throw new IllegalArgumentException("format string may not be null"); + } + + if (locale == null) { + throw new IllegalArgumentException("locale may not be null"); + } + + this.locale = locale; + this.formatString = formatString; + dateFormat = null; + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with he given date format. + * + * @param dateFormat + * the date format to use when rendering dates + * @throws IllegalArgumentException + * if {@code dateFormat} is {@code null} + */ + public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException { + super(Date.class); + if (dateFormat == null) { + throw new IllegalArgumentException("date format may not be null"); + } + + locale = null; + formatString = null; + this.dateFormat = dateFormat; + } + + @Override + public String encode(Date value) { + if (dateFormat != null) { + return dateFormat.format(value); + } else { + return String.format(locale, formatString, value); + } + } + + @Override + public String toString() { + final String fieldInfo; + if (dateFormat != null) { + fieldInfo = "dateFormat: " + dateFormat.toString(); + } else { + fieldInfo = "locale: " + locale + ", formatString: " + formatString; + } + + return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java new file mode 100644 index 0000000000..6439608c20 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting HTML content. + * + * @since + * @author Vaadin Ltd + */ +public class HtmlRenderer extends AbstractRenderer<String> { + /** + * Creates a new HTML renderer. + */ + public HtmlRenderer() { + super(String.class); + } + + @Override + public String encode(String value) { + return value; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java new file mode 100644 index 0000000000..12fcfc890a --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java @@ -0,0 +1,159 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.renderers; + +import java.text.NumberFormat; +import java.util.Locale; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting number values. + * + * @since + * @author Vaadin Ltd + */ +public class NumberRenderer extends AbstractRenderer<Number> { + private final Locale locale; + private final NumberFormat numberFormat; + private final String formatString; + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the number's natural string + * representation in the default locale. + */ + public NumberRenderer() { + this(Locale.getDefault()); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render the number as defined with the given + * number format. + * + * @param numberFormat + * the number format with which to display numbers + * @throws IllegalArgumentException + * if {@code numberFormat} is {@code null} + */ + public NumberRenderer(NumberFormat numberFormat) + throws IllegalArgumentException { + super(Number.class); + + if (numberFormat == null) { + throw new IllegalArgumentException("Number format may not be null"); + } + + locale = null; + this.numberFormat = numberFormat; + formatString = null; + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the number's natural string + * representation in the given locale. + * + * @param locale + * the locale in which to display numbers + * @throws IllegalArgumentException + * if {@code locale} is {@code null} + */ + public NumberRenderer(Locale locale) throws IllegalArgumentException { + this("%s", locale); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the given format string in the + * default locale. + * + * @param formatString + * the format string with which to format the number + * @throws IllegalArgumentException + * if {@code formatString} is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public NumberRenderer(String formatString) throws IllegalArgumentException { + this(formatString, Locale.getDefault()); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the given format string in the + * given locale. + * + * @param formatString + * the format string with which to format the number + * @param locale + * the locale in which to present numbers + * @throws IllegalArgumentException + * if either argument is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public NumberRenderer(String formatString, Locale locale) { + super(Number.class); + + if (formatString == null) { + throw new IllegalArgumentException("Format string may not be null"); + } + + if (locale == null) { + throw new IllegalArgumentException("Locale may not be null"); + } + + this.locale = locale; + numberFormat = null; + this.formatString = formatString; + } + + @Override + public String encode(Number value) { + if (formatString != null && locale != null) { + return String.format(locale, formatString, value); + } else if (numberFormat != null) { + return numberFormat.format(value); + } else { + throw new IllegalStateException(String.format("Internal bug: " + + "%s is in an illegal state: " + + "[locale: %s, numberFormat: %s, formatString: %s]", + getClass().getSimpleName(), locale, numberFormat, + formatString)); + } + } + + @Override + public String toString() { + final String fieldInfo; + if (numberFormat != null) { + fieldInfo = "numberFormat: " + numberFormat.toString(); + } else { + fieldInfo = "locale: " + locale + ", formatString: " + formatString; + } + + return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java new file mode 100644 index 0000000000..61348a9e49 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.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.ui.components.grid.renderers; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +/** + * A renderer for presenting simple plain-text string values. + * + * @since + * @author Vaadin Ltd + */ +public class TextRenderer extends AbstractRenderer<String> { + + /** + * Creates a new text renderer + */ + public TextRenderer() { + super(String.class); + } + + @Override + public Object encode(String value) { + return value; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java new file mode 100644 index 0000000000..e153b8a4e4 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; + +import com.vaadin.ui.components.grid.Grid; + +/** + * A base class for SelectionModels that contains some of the logic that is + * reusable. + * + * @since + * @author Vaadin Ltd + */ +public abstract class AbstractSelectionModel implements SelectionModel { + protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>(); + protected Grid grid = null; + + @Override + public boolean isSelected(final Object itemId) { + return selection.contains(itemId); + } + + @Override + public Collection<Object> getSelectedRows() { + return new ArrayList<Object>(selection); + } + + @Override + public void setGrid(final Grid grid) { + this.grid = grid; + } + + /** + * Fires a {@link SelectionChangeEvent} to all the + * {@link SelectionChangeListener SelectionChangeListeners} currently added + * to the Grid in which this SelectionModel is. + * <p> + * Note that this is only a helper method, and routes the call all the way + * to Grid. A {@link SelectionModel} is not a + * {@link SelectionChangeNotifier} + * + * @param oldSelection + * the complete {@link Collection} of the itemIds that were + * selected <em>before</em> this event happened + * @param newSelection + * the complete {@link Collection} of the itemIds that are + * selected <em>after</em> this event happened + */ + protected void fireSelectionChangeEvent( + final Collection<Object> oldSelection, + final Collection<Object> newSelection) { + grid.fireSelectionChangeEvent(oldSelection, newSelection); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java new file mode 100644 index 0000000000..602e5ca169 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java @@ -0,0 +1,138 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import com.vaadin.data.Container.Indexed; + +/** + * A default implementation of a {@link SelectionModel.Multi} + * + * @since + * @author Vaadin Ltd + */ +public class MultiSelectionModel extends AbstractSelectionModel implements + SelectionModel.Multi { + + @Override + public boolean select(final Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + // select will fire the event + return select(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + + @Override + public boolean select(final Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + final boolean hasSomeDifferingElements = !selection + .containsAll(itemIds); + if (hasSomeDifferingElements) { + final HashSet<Object> oldSelection = new HashSet<Object>(selection); + selection.addAll(itemIds); + fireSelectionChangeEvent(oldSelection, selection); + } + return hasSomeDifferingElements; + } + + @Override + public boolean deselect(final Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + // deselect will fire the event + return deselect(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + + @Override + public boolean deselect(final Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + final boolean hasCommonElements = !Collections.disjoint(itemIds, + selection); + if (hasCommonElements) { + final HashSet<Object> oldSelection = new HashSet<Object>(selection); + selection.removeAll(itemIds); + fireSelectionChangeEvent(oldSelection, selection); + } + return hasCommonElements; + } + + @Override + public boolean selectAll() { + // select will fire the event + final Indexed container = grid.getContainerDatasource(); + if (container != null) { + return select(container.getItemIds()); + } else if (selection.isEmpty()) { + return false; + } else { + /* + * this should never happen (no container but has a selection), but + * I guess the only theoretically correct course of action... + */ + return deselectAll(); + } + } + + @Override + public boolean deselectAll() { + // deselect will fire the event + return deselect(getSelectedRows()); + } + + /** + * {@inheritDoc} + * <p> + * The returned Collection is in <strong>order of selection</strong> – + * the item that was first selected will be first in the collection, and so + * on. Should an item have been selected twice without being deselected in + * between, it will have remained in its original position. + */ + @Override + public Collection<Object> getSelectedRows() { + // overridden only for JavaDoc + return super.getSelectedRows(); + } + + /** + * Resets the selection model. + * <p> + * Equivalent to calling {@link #deselectAll()} + */ + @Override + public void reset() { + deselectAll(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java new file mode 100644 index 0000000000..89c31398ea --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.ui.components.grid.Grid; + +/** + * A default implementation for a {@link SelectionModel.None} + * + * @since + * @author Vaadin Ltd + */ +public class NoSelectionModel implements SelectionModel.None { + @Override + public void setGrid(final Grid grid) { + // NOOP, not needed for anything + } + + @Override + public boolean isSelected(final Object itemId) { + return false; + } + + @Override + public Collection<Object> getSelectedRows() { + return Collections.emptyList(); + } + + /** + * Semantically resets the selection model. + * <p> + * Effectively a no-op. + */ + @Override + public void reset() { + // NOOP + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java new file mode 100644 index 0000000000..af6a37dfde --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Collection; +import java.util.EventObject; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.vaadin.ui.components.grid.Grid; + +/** + * An event that specifies what in a selection has changed, and where the + * selection took place. + * + * @since + * @author Vaadin Ltd + */ +public class SelectionChangeEvent extends EventObject { + + private LinkedHashSet<Object> oldSelection; + private LinkedHashSet<Object> newSelection; + + public SelectionChangeEvent(Grid source, Collection<Object> oldSelection, + Collection<Object> newSelection) { + super(source); + this.oldSelection = new LinkedHashSet<Object>(oldSelection); + this.newSelection = new LinkedHashSet<Object>(newSelection); + } + + /** + * A {@link Collection} of all the itemIds that became selected. + * <p> + * <em>Note:</em> this excludes all itemIds that might have been previously + * selected. + * + * @return a Collection of the itemIds that became selected + */ + public Set<Object> getAdded() { + return Sets.difference(newSelection, oldSelection); + } + + /** + * A {@link Collection} of all the itemIds that became deselected. + * <p> + * <em>Note:</em> this excludes all itemIds that might have been previously + * deselected. + * + * @return a Collection of the itemIds that became deselected + */ + public Set<Object> getRemoved() { + return Sets.difference(oldSelection, newSelection); + } + + @Override + public Grid getSource() { + return (Grid) super.getSource(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java new file mode 100644 index 0000000000..0d10e8c74d --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.io.Serializable; + +/** + * The listener interface for receiving {@link SelectionChangeEvent + * SelectionChangeEvents}. + * + * @since + * @author Vaadin Ltd + */ +public interface SelectionChangeListener extends Serializable { + /** + * Notifies the listener that the selection state has changed. + * + * @param event + * the selection change event + */ + void selectionChange(SelectionChangeEvent event); +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java new file mode 100644 index 0000000000..40cef965dd --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.io.Serializable; + +/** + * The interface for adding and removing listeners for + * {@link SelectionChangeEvent SelectionChangeEvents}. + * + * @since + * @author Vaadin Ltd + */ +public interface SelectionChangeNotifier extends Serializable { + /** + * Registers a new selection change listener + * + * @param listener + * the listener to register + */ + void addSelectionChangeListener(SelectionChangeListener listener); + + /** + * Removes a previously registered selection change listener + * + * @param listener + * the listener to remove + */ + void removeSelectionChangeListener(SelectionChangeListener listener); +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java new file mode 100644 index 0000000000..60bb130ab1 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java @@ -0,0 +1,234 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.io.Serializable; +import java.util.Collection; + +import com.vaadin.ui.components.grid.Grid; + +/** + * The server-side interface that controls Grid's selection state. + * + * @since + * @author Vaadin Ltd + */ +public interface SelectionModel extends Serializable { + /** + * Checks whether an item is selected or not. + * + * @param itemId + * the item id to check for + * @return <code>true</code> iff the item is selected + */ + boolean isSelected(Object itemId); + + /** + * Returns a collection of all the currently selected itemIds. + * + * @return a collection of all the currently selected itemIds + */ + Collection<Object> getSelectedRows(); + + /** + * Injects the current {@link Grid} instance into the SelectionModel. + * <p> + * <em>Note:</em> This method should not be called manually. + * + * @param grid + * the Grid in which the SelectionModel currently is, or + * <code>null</code> when a selection model is being detached + * from a Grid. + */ + void setGrid(Grid grid); + + /** + * Resets the SelectiomModel to an initial state. + * <p> + * Most often this means that the selection state is cleared, but + * implementations are free to interpret the "initial state" as they wish. + * Some, for example, may want to keep the first selected item as selected. + */ + void reset(); + + /** + * A SelectionModel that supports multiple selections to be made. + * <p> + * This interface has a contract of having the same behavior, no matter how + * the selection model is interacted with. In other words, if something is + * forbidden to do in e.g. the user interface, it must also be forbidden to + * do in the server-side and client-side APIs. + */ + public interface Multi extends SelectionModel { + + /** + * Marks items as selected. + * <p> + * This method does not clear any previous selection state, only adds to + * it. + * + * @param itemIds + * the itemId(s) to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if the <code>itemIds</code> varargs array is + * <code>null</code> + * @see #deselect(Object...) + */ + boolean select(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as selected. + * <p> + * This method does not clear any previous selection state, only adds to + * it. + * + * @param itemIds + * the itemIds to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if <code>itemIds</code> is <code>null</code> + * @see #deselect(Collection) + */ + boolean select(Collection<?> itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if none the given itemIds were selected + * previously + * @throws IllegalArgumentException + * if the <code>itemIds</code> varargs array is + * <code>null</code> + * @see #select(Object...) + */ + boolean deselect(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if none the given itemIds were selected + * previously + * @throws IllegalArgumentException + * if <code>itemIds</code> is <code>null</code> + * @see #select(Collection) + */ + boolean deselect(Collection<?> itemIds) throws IllegalArgumentException; + + /** + * Marks all the items in the current Container as selected + * + * @return <code>true</code> iff some items were previously not selected + * @see #deselectAll() + */ + boolean selectAll(); + + /** + * Marks all the items in the current Container as deselected + * + * @return <code>true</code> iff some items were previously selected + * @see #selectAll() + */ + boolean deselectAll(); + } + + /** + * A SelectionModel that supports for only single rows to be selected at a + * time. + * <p> + * This interface has a contract of having the same behavior, no matter how + * the selection model is interacted with. In other words, if something is + * forbidden to do in e.g. the user interface, it must also be forbidden to + * do in the server-side and client-side APIs. + */ + public interface Single extends SelectionModel { + /** + * Marks an item as selected. + * + * @param itemIds + * the itemId to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalStateException + * if the selection was illegal. One such reason might be + * that the implementation already had an item selected, and + * that needs to be explicitly deselected before + * re-selecting something + * @see #deselect(Object) + */ + boolean select(Object itemId) throws IllegalStateException; + + /** + * Marks an item as deselected. + * + * @param itemId + * the itemId to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalStateException + * if the deselection was illegal. One such reason might be + * that the implementation enforces that an item is always + * selected + * @see #select(Object) + */ + boolean deselect(Object itemId) throws IllegalStateException; + + /** + * Gets the item id of the currently selected item. + * + * @return the item id of the currently selected item, or + * <code>null</code> if nothing is selected + */ + Object getSelectedRow(); + } + + /** + * A SelectionModel that does not allow for rows to be selected. + * <p> + * This interface has a contract of having the same behavior, no matter how + * the selection model is interacted with. In other words, if the developer + * is unable to select something programmatically, it is not allowed for the + * end-user to select anything, either. + */ + public interface None extends SelectionModel { + + /** + * {@inheritDoc} + * + * @return always <code>false</code>. + */ + @Override + public boolean isSelected(Object itemId); + + /** + * {@inheritDoc} + * + * @return always an empty collection. + */ + @Override + public Collection<Object> getSelectedRows(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java new file mode 100644 index 0000000000..0f6e8a296d --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.selection; + +import java.util.Collection; +import java.util.Collections; + +/** + * A default implementation of a {@link SelectionModel.Single} + * + * @since + * @author Vaadin Ltd + */ +public class SingleSelectionModel extends AbstractSelectionModel implements + SelectionModel.Single { + @Override + public boolean select(final Object itemId) { + final Object selectedRow = getSelectedRow(); + final boolean modified = selection.add(itemId); + if (modified) { + final Collection<Object> deselected; + if (selectedRow != null) { + deselectInternal(selectedRow, false); + deselected = Collections.singleton(selectedRow); + } else { + deselected = Collections.emptySet(); + } + + fireSelectionChangeEvent(deselected, selection); + } + + return modified; + } + + @Override + public boolean deselect(final Object itemId) { + return deselectInternal(itemId, true); + } + + private boolean deselectInternal(final Object itemId, + boolean fireEventIfNeeded) { + final boolean modified = selection.remove(itemId); + if (fireEventIfNeeded && modified) { + fireSelectionChangeEvent(Collections.singleton(itemId), + Collections.emptySet()); + } + return modified; + } + + @Override + public Object getSelectedRow() { + if (selection.isEmpty()) { + return null; + } else { + return selection.iterator().next(); + } + } + + /** + * Resets the selection state. + * <p> + * If an item is selected, it will become deselected. + */ + @Override + public void reset() { + deselect(getSelectedRow()); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/sort/Sort.java b/server/src/com/vaadin/ui/components/grid/sort/Sort.java new file mode 100644 index 0000000000..54831378b6 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/sort/Sort.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.sort; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Fluid Sort API. Provides a convenient, human-readable way of specifying + * multi-column sort order. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class Sort implements Serializable { + + private final Sort previous; + private final SortOrder order; + + /** + * Initial constructor, called by the static by() methods. + * + * @param propertyId + * a property ID, corresponding to a property in the data source + * @param direction + * a sort direction value + */ + private Sort(Object propertyId, SortDirection direction) { + previous = null; + order = new SortOrder(propertyId, direction); + } + + /** + * Chaining constructor, called by the non-static then() methods. This + * constructor links to the previous Sort object. + * + * @param previous + * the sort marker that comes before this one + * @param propertyId + * a property ID, corresponding to a property in the data source + * @param direction + * a sort direction value + */ + private Sort(Sort previous, Object propertyId, SortDirection direction) { + this.previous = previous; + order = new SortOrder(propertyId, direction); + + Sort s = previous; + while (s != null) { + if (s.order.getPropertyId() == propertyId) { + throw new IllegalStateException( + "Can not sort along the same property (" + propertyId + + ") twice!"); + } + s = s.previous; + } + + } + + /** + * Start building a Sort order by sorting a provided column in ascending + * order. + * + * @param propertyId + * a property id, corresponding to a data source property + * @return a sort object + */ + public static Sort by(Object propertyId) { + return by(propertyId, SortDirection.ASCENDING); + } + + /** + * Start building a Sort order by sorting a provided column. + * + * @param propertyId + * a property id, corresponding to a data source property + * @param direction + * a sort direction value + * @return a sort object + */ + public static Sort by(Object propertyId, SortDirection direction) { + return new Sort(propertyId, direction); + } + + /** + * Continue building a Sort order. The provided property is sorted in + * ascending order if the previously added properties have been evaluated as + * equals. + * + * @param propertyId + * a property id, corresponding to a data source property + * @return a sort object + */ + public Sort then(Object propertyId) { + return then(propertyId, SortDirection.ASCENDING); + } + + /** + * Continue building a Sort order. The provided property is sorted in + * specified order if the previously added properties have been evaluated as + * equals. + * + * @param propertyId + * a property id, corresponding to a data source property + * @param direction + * a sort direction value + * @return a sort object + */ + public Sort then(Object propertyId, SortDirection direction) { + return new Sort(this, propertyId, direction); + } + + /** + * Build a sort order list, ready to be passed to Grid + * + * @return a sort order list. + */ + public List<SortOrder> build() { + + int count = 1; + Sort s = this; + while (s.previous != null) { + s = s.previous; + ++count; + } + + List<SortOrder> order = new ArrayList<SortOrder>(count); + + s = this; + do { + order.add(0, s.order); + s = s.previous; + } while (s != null); + + return order; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java new file mode 100644 index 0000000000..a76148fe0c --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.components.grid.sort; + +import java.io.Serializable; + +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Sort order descriptor. Links together a {@link SortDirection} value and a + * Vaadin container property ID. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class SortOrder implements Serializable { + + private final Object propertyId; + private final SortDirection direction; + + /** + * Create a SortOrder object. Both arguments must be non-null. + * + * @param propertyId + * id of the data source property to sort by + * @param direction + * value indicating whether the property id should be sorted in + * ascending or descending order + */ + public SortOrder(Object propertyId, SortDirection direction) { + if (propertyId == null) { + throw new IllegalArgumentException("Property ID can not be null!"); + } + if (direction == null) { + throw new IllegalArgumentException( + "Direction value can not be null!"); + } + this.propertyId = propertyId; + this.direction = direction; + } + + /** + * Returns the property ID. + * + * @return a property ID + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * Returns the {@link SortDirection} value. + * + * @return a sort direction value + */ + public SortDirection getDirection() { + return direction; + } + + @Override + public String toString() { + return propertyId + " " + direction; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + direction.hashCode(); + result = prime * result + propertyId.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (getClass() != obj.getClass()) { + return false; + } + + SortOrder other = (SortOrder) obj; + if (direction != other.direction) { + return false; + } else if (!propertyId.equals(other.propertyId)) { + return false; + } + return true; + } + +} diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java index b58d962d96..463f0c92c1 100644 --- a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java @@ -10,8 +10,15 @@ import java.util.Map; import org.junit.Assert; +import org.easymock.Capture; +import org.easymock.EasyMock; + import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeListener; import com.vaadin.data.Item; +import com.vaadin.data.util.filter.Compare; /** * Test basic functionality of BeanItemContainer. @@ -727,4 +734,182 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest { assertNull(container.getContainerProperty(john, "address.street") .getValue()); } + + public void testItemAddedEvent() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class)); + EasyMock.replay(addListener); + + container.addItem(bean); + + EasyMock.verify(addListener); + } + + public void testItemAddedEvent_AddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItem(bean); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemAddedEvent_addItemAt_IndexOfAddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItemAt(1, new Person("")); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemAddedEvent_addItemAfter_IndexOfAddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItemAfter(bean, new Person("")); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemAddedEvent_amountOfAddedItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), new Person( + "John")); + + container.addAll(beans); + + assertEquals(2, capturedEvent.getValue().getAddedItemsCount()); + } + + public void testItemAddedEvent_someItemsAreFiltered_amountOfAddedItemsIsReducedByAmountOfFilteredItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), new Person( + "John")); + container.addFilter(new Compare.Equal("name", "John")); + + container.addAll(beans); + + assertEquals(1, capturedEvent.getValue().getAddedItemsCount()); + } + + public void testItemAddedEvent_someItemsAreFiltered_addedItemIsTheFirstVisibleItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), bean); + container.addFilter(new Compare.Equal("name", "John")); + + container.addAll(beans); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + removeListener.containerItemSetChange(EasyMock + .isA(ItemRemoveEvent.class)); + EasyMock.replay(removeListener); + + container.removeItem(bean); + + EasyMock.verify(removeListener); + } + + public void testItemRemovedEvent_RemovedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(bean); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent_indexOfRemovedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + container.addItem(new Person("Jack")); + Person secondBean = new Person("John"); + container.addItem(secondBean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(secondBean); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent_amountOfRemovedItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + container.addItem(new Person("Jack")); + container.addItem(new Person("John")); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeAllItems(); + + assertEquals(2, capturedEvent.getValue().getRemovedItemsCount()); + } + + private Capture<ItemAddEvent> captureAddEvent( + ItemSetChangeListener addListener) { + Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>(); + addListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private Capture<ItemRemoveEvent> captureRemoveEvent( + ItemSetChangeListener removeListener) { + Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>(); + removeListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private ItemSetChangeListener createListenerMockFor( + BeanItemContainer<Person> container) { + ItemSetChangeListener listener = EasyMock + .createNiceMock(ItemSetChangeListener.class); + container.addItemSetChangeListener(listener); + return listener; + } } diff --git a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java index eacee7e301..ddfee103c3 100644 --- a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java +++ b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java @@ -4,6 +4,12 @@ import java.util.List; import org.junit.Assert; +import org.easymock.Capture; +import org.easymock.EasyMock; + +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeListener; import com.vaadin.data.Item; public class TestIndexedContainer extends AbstractInMemoryContainerTest { @@ -271,6 +277,113 @@ public class TestIndexedContainer extends AbstractInMemoryContainerTest { counter.assertNone(); } + public void testItemAddedEvent() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class)); + EasyMock.replay(addListener); + + container.addItem(); + + EasyMock.verify(addListener); + } + + public void testItemAddedEvent_AddedItem() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + Object itemId = container.addItem(); + + assertEquals(itemId, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemAddedEvent_IndexOfAddedItem() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + container.addItem(); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + Object itemId = container.addItemAt(1); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent() { + IndexedContainer container = new IndexedContainer(); + Object itemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + removeListener.containerItemSetChange(EasyMock + .isA(ItemRemoveEvent.class)); + EasyMock.replay(removeListener); + + container.removeItem(itemId); + + EasyMock.verify(removeListener); + } + + public void testItemRemovedEvent_RemovedItem() { + IndexedContainer container = new IndexedContainer(); + Object itemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(itemId); + + assertEquals(itemId, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent_indexOfRemovedItem() { + IndexedContainer container = new IndexedContainer(); + container.addItem(); + Object secondItemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(secondItemId); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent_amountOfRemovedItems() { + IndexedContainer container = new IndexedContainer(); + container.addItem(); + container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeAllItems(); + + assertEquals(2, capturedEvent.getValue().getRemovedItemsCount()); + } + + private Capture<ItemAddEvent> captureAddEvent( + ItemSetChangeListener addListener) { + Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>(); + addListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private Capture<ItemRemoveEvent> captureRemoveEvent( + ItemSetChangeListener removeListener) { + Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>(); + removeListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private ItemSetChangeListener createListenerMockFor( + IndexedContainer container) { + ItemSetChangeListener listener = EasyMock + .createNiceMock(ItemSetChangeListener.class); + container.addItemSetChangeListener(listener); + return listener; + } + // Ticket 8028 public void testGetItemIdsRangeIndexOutOfBounds() { IndexedContainer ic = new IndexedContainer(); diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java new file mode 100644 index 0000000000..9ecf131c5b --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; +import com.vaadin.data.util.IndexedContainer; + +public class DataProviderExtension { + private RpcDataProviderExtension dataProvider; + private DataProviderKeyMapper keyMapper; + private Container.Indexed container; + + private static final Object ITEM_ID1 = "itemid1"; + private static final Object ITEM_ID2 = "itemid2"; + private static final Object ITEM_ID3 = "itemid3"; + + private static final Object PROPERTY_ID1_STRING = "property1"; + + @Before + public void setup() { + container = new IndexedContainer(); + populate(container); + + dataProvider = new RpcDataProviderExtension(container); + keyMapper = dataProvider.getKeyMapper(); + } + + private static void populate(Indexed container) { + container.addContainerProperty(PROPERTY_ID1_STRING, String.class, ""); + for (Object itemId : Arrays.asList(ITEM_ID1, ITEM_ID2, ITEM_ID3)) { + final Item item = container.addItem(itemId); + @SuppressWarnings("unchecked") + final Property<String> stringProperty = item + .getItemProperty(PROPERTY_ID1_STRING); + stringProperty.setValue(itemId.toString()); + } + } + + @Test + public void pinBasics() { + assertFalse("itemId1 should not start as pinned", + keyMapper.isPinned(ITEM_ID2)); + + keyMapper.pin(ITEM_ID1); + assertTrue("itemId1 should now be pinned", keyMapper.isPinned(ITEM_ID1)); + + keyMapper.unpin(ITEM_ID1); + assertFalse("itemId1 should not be pinned anymore", + keyMapper.isPinned(ITEM_ID2)); + } + + @Test(expected = IllegalStateException.class) + public void doublePinning() { + keyMapper.pin(ITEM_ID1); + keyMapper.pin(ITEM_ID1); + } + + @Test(expected = IllegalStateException.class) + public void nonexistentUnpin() { + keyMapper.unpin(ITEM_ID1); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java new file mode 100644 index 0000000000..d1c821cc54 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.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.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +public class GridColumns { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper<Object> columnIdMapper; + + @Before + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGeneration() throws Exception { + + for (Object propertyId : grid.getContainerDatasource() + .getContainerPropertyIds()) { + + // All property ids should get a column + GridColumn column = grid.getColumn(propertyId); + assertNotNull(column); + + // Property id should be the column header by default + assertEquals(propertyId.toString(), grid.getHeader() + .getDefaultRow().getCell(propertyId).getText()); + } + } + + @Test + public void testModifyingColumnProperties() throws Exception { + + // Modify first column + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + column.setHeaderCaption("CustomHeader"); + assertEquals("CustomHeader", column.getHeaderCaption()); + assertEquals(column.getHeaderCaption(), + getColumnState("column1").header); + + column.setVisible(false); + assertFalse(column.isVisible()); + assertFalse(getColumnState("column1").visible); + + column.setVisible(true); + assertTrue(column.isVisible()); + assertTrue(getColumnState("column1").visible); + + column.setWidth(100); + assertEquals(100, column.getWidth()); + assertEquals(column.getWidth(), getColumnState("column1").width); + + try { + column.setWidth(-1); + fail("Setting width to -1 should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + assertEquals(100, column.getWidth()); + assertEquals(100, getColumnState("column1").width); + } + + @Test + public void testRemovingColumn() throws Exception { + + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + // Remove column + grid.getContainerDatasource().removeContainerProperty("column1"); + + try { + column.setHeaderCaption("asd"); + + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setFooterCaption("asd"); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setVisible(false); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setWidth(123); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + assertNull(grid.getColumn("column1")); + assertNull(getColumnState("column1")); + } + + @Test + public void testAddingColumn() throws Exception { + grid.getContainerDatasource().addContainerProperty("columnX", + String.class, ""); + GridColumn column = grid.getColumn("columnX"); + assertNotNull(column); + } + + @Test + public void testHeaderVisiblility() throws Exception { + + assertTrue(grid.getHeader().isVisible()); + assertTrue(state.header.visible); + + grid.getHeader().setVisible(false); + assertFalse(grid.getHeader().isVisible()); + assertFalse(state.header.visible); + + grid.getHeader().setVisible(true); + assertTrue(grid.getHeader().isVisible()); + assertTrue(state.header.visible); + } + + @Test + public void testFooterVisibility() throws Exception { + + assertFalse(grid.getFooter().isVisible()); + assertFalse(state.footer.visible); + + grid.getFooter().setVisible(true); + assertTrue(grid.getFooter().isVisible()); + assertTrue(state.footer.visible); + + grid.getFooter().setVisible(false); + assertFalse(grid.getFooter().isVisible()); + assertFalse(state.footer.visible); + } + + @Test + public void testFrozenColumnByPropertyId() { + assertNull("Grid should not start with a frozen column", + grid.getLastFrozenPropertyId()); + + Object propertyId = grid.getContainerDatasource() + .getContainerPropertyIds().iterator().next(); + grid.setLastFrozenPropertyId(propertyId); + assertEquals(propertyId, grid.getLastFrozenPropertyId()); + + grid.getContainerDatasource().removeContainerProperty(propertyId); + assertNull(grid.getLastFrozenPropertyId()); + } + + private GridColumnState getColumnState(Object propertyId) { + String columnId = columnIdMapper.key(propertyId); + for (GridColumnState columnState : state.columns) { + if (columnState.id.equals(columnId)) { + return columnState; + } + } + return null; + } + +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java new file mode 100644 index 0000000000..7993d31295 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java @@ -0,0 +1,306 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.Grid.SelectionMode; +import com.vaadin.ui.components.grid.selection.SelectionChangeEvent; +import com.vaadin.ui.components.grid.selection.SelectionChangeListener; +import com.vaadin.ui.components.grid.selection.SelectionModel; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridSelection { + + private static class MockSelectionChangeListener implements + SelectionChangeListener { + private SelectionChangeEvent event; + + @Override + public void selectionChange(final SelectionChangeEvent event) { + this.event = event; + } + + public Collection<?> getAdded() { + return event.getAdded(); + } + + public Collection<?> getRemoved() { + return event.getRemoved(); + } + + public void clearEvent() { + /* + * This method is not strictly needed as the event will simply be + * overridden, but it's good practice, and makes the code more + * obvious. + */ + event = null; + } + + public boolean eventHasHappened() { + return event != null; + } + } + + private Grid grid; + private MockSelectionChangeListener mockListener; + + private final Object itemId1Present = "itemId1Present"; + private final Object itemId2Present = "itemId2Present"; + + private final Object itemId1NotPresent = "itemId1NotPresent"; + private final Object itemId2NotPresent = "itemId2NotPresent"; + + @Before + public void setup() { + final IndexedContainer container = new IndexedContainer(); + container.addItem(itemId1Present); + container.addItem(itemId2Present); + for (int i = 2; i < 10; i++) { + container.addItem(new Object()); + } + + assertEquals("init size", 10, container.size()); + assertTrue("itemId1Present", container.containsId(itemId1Present)); + assertTrue("itemId2Present", container.containsId(itemId2Present)); + assertFalse("itemId1NotPresent", + container.containsId(itemId1NotPresent)); + assertFalse("itemId2NotPresent", + container.containsId(itemId2NotPresent)); + + grid = new Grid(container); + + mockListener = new MockSelectionChangeListener(); + grid.addSelectionChangeListener(mockListener); + + assertFalse("eventHasHappened", mockListener.eventHasHappened()); + } + + @Test + public void defaultSelectionModeIsMulti() { + assertTrue(grid.getSelectionModel() instanceof SelectionModel.Multi); + } + + @Test(expected = IllegalStateException.class) + public void getSelectedRowThrowsExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.getSelectedRow(); + } + + @Test(expected = IllegalStateException.class) + public void getSelectedRowThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.getSelectedRow(); + } + + @Test(expected = IllegalStateException.class) + public void selectThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.select(itemId1Present); + } + + @Test(expected = IllegalStateException.class) + public void deselectRowThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.deselect(itemId1Present); + } + + @Test + public void selectionModeMapsToMulti() { + assertTrue(grid.setSelectionMode(SelectionMode.MULTI) instanceof SelectionModel.Multi); + } + + @Test + public void selectionModeMapsToSingle() { + assertTrue(grid.setSelectionMode(SelectionMode.SINGLE) instanceof SelectionModel.Single); + } + + @Test + public void selectionModeMapsToNone() { + assertTrue(grid.setSelectionMode(SelectionMode.NONE) instanceof SelectionModel.None); + } + + @Test(expected = IllegalArgumentException.class) + public void selectionModeNullThrowsException() { + grid.setSelectionMode(null); + } + + @Test + public void noSelectModel_isSelected() { + grid.setSelectionMode(SelectionMode.NONE); + assertFalse("itemId1Present", grid.isSelected(itemId1Present)); + assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent)); + } + + @Test(expected = IllegalStateException.class) + public void noSelectModel_getSelectedRow() { + grid.setSelectionMode(SelectionMode.NONE); + grid.getSelectedRow(); + } + + @Test + public void noSelectModel_getSelectedRows() { + grid.setSelectionMode(SelectionMode.NONE); + assertTrue(grid.getSelectedRows().isEmpty()); + } + + @Test + public void selectionCallsListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + selectionCallsListener(); + } + + @Test + public void selectionCallsListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + selectionCallsListener(); + } + + private void selectionCallsListener() { + grid.select(itemId1Present); + assertEquals("added size", 1, mockListener.getAdded().size()); + assertEquals("added item", itemId1Present, mockListener.getAdded() + .iterator().next()); + assertEquals("removed size", 0, mockListener.getRemoved().size()); + } + + @Test + public void deselectionCallsListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + deselectionCallsListener(); + } + + @Test + public void deselectionCallsListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + deselectionCallsListener(); + } + + private void deselectionCallsListener() { + grid.select(itemId1Present); + mockListener.clearEvent(); + + grid.deselect(itemId1Present); + assertEquals("removed size", 1, mockListener.getRemoved().size()); + assertEquals("removed item", itemId1Present, mockListener.getRemoved() + .iterator().next()); + assertEquals("removed size", 0, mockListener.getAdded().size()); + } + + @Test + public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + deselectPresentButNotSelectedItemIdShouldntFireListener(); + } + + @Test + public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + deselectPresentButNotSelectedItemIdShouldntFireListener(); + } + + private void deselectPresentButNotSelectedItemIdShouldntFireListener() { + grid.deselect(itemId1Present); + assertFalse(mockListener.eventHasHappened()); + } + + @Test + public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.deselect(itemId1NotPresent); + } + + @Test + public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.deselect(itemId1NotPresent); + } + + @Test + public void selectNotPresentItemIdShouldNotThrowExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.select(itemId1NotPresent); + } + + @Test + public void selectNotPresentItemIdShouldNotThrowExceptionSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.select(itemId1NotPresent); + } + + @Test + public void selectAllMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + final SelectionModel.Multi select = (SelectionModel.Multi) grid + .getSelectionModel(); + select.selectAll(); + assertEquals("added size", 10, mockListener.getAdded().size()); + assertEquals("removed size", 0, mockListener.getRemoved().size()); + assertTrue("itemId1Present", + mockListener.getAdded().contains(itemId1Present)); + assertTrue("itemId2Present", + mockListener.getAdded().contains(itemId2Present)); + } + + @Test + public void deselectAllMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + final SelectionModel.Multi select = (SelectionModel.Multi) grid + .getSelectionModel(); + select.selectAll(); + mockListener.clearEvent(); + + select.deselectAll(); + assertEquals("removed size", 10, mockListener.getRemoved().size()); + assertEquals("added size", 0, mockListener.getAdded().size()); + assertTrue("itemId1Present", + mockListener.getRemoved().contains(itemId1Present)); + assertTrue("itemId2Present", + mockListener.getRemoved().contains(itemId2Present)); + assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty()); + } + + @Test + public void reselectionDeselectsPreviousSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.select(itemId1Present); + mockListener.clearEvent(); + + grid.select(itemId2Present); + assertEquals("added size", 1, mockListener.getAdded().size()); + assertEquals("removed size", 1, mockListener.getRemoved().size()); + assertEquals("added item", itemId2Present, mockListener.getAdded() + .iterator().next()); + assertEquals("removed item", itemId1Present, mockListener.getRemoved() + .iterator().next()); + assertEquals("selectedRows is correct", itemId2Present, + grid.getSelectedRow()); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java new file mode 100644 index 0000000000..e89f6a8c6e --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridFooter; +import com.vaadin.ui.components.grid.GridFooter.FooterRow; +import com.vaadin.ui.components.grid.GridHeader; +import com.vaadin.ui.components.grid.GridHeader.HeaderRow; + +public class GridStaticSection { + + private Indexed dataSource = new IndexedContainer(); + private Grid grid; + + @Before + public void setUp() { + dataSource.addContainerProperty("firstName", String.class, ""); + dataSource.addContainerProperty("lastName", String.class, ""); + dataSource.addContainerProperty("streetAddress", String.class, ""); + dataSource.addContainerProperty("zipCode", Integer.class, null); + grid = new Grid(dataSource); + } + + @Test + public void testAddAndRemoveHeaders() { + + final GridHeader section = grid.getHeader(); + assertEquals(1, section.getRowCount()); + section.prependRow(); + assertEquals(2, section.getRowCount()); + section.removeRow(0); + assertEquals(1, section.getRowCount()); + section.removeRow(0); + assertEquals(0, section.getRowCount()); + assertEquals(null, section.getDefaultRow()); + HeaderRow row = section.appendRow(); + assertEquals(1, section.getRowCount()); + assertEquals(null, section.getDefaultRow()); + section.setDefaultRow(row); + assertEquals(row, section.getDefaultRow()); + } + + @Test + public void testAddAndRemoveFooters() { + final GridFooter section = grid.getFooter(); + + // By default there are no footer rows + assertEquals(0, section.getRowCount()); + FooterRow row = section.appendRow(); + + assertEquals(1, section.getRowCount()); + section.prependRow(); + assertEquals(2, section.getRowCount()); + assertEquals(row, section.getRow(1)); + section.removeRow(0); + assertEquals(1, section.getRowCount()); + section.removeRow(0); + assertEquals(0, section.getRowCount()); + } + + @Test + public void testJoinHeaderCells() { + final GridHeader section = grid.getHeader(); + HeaderRow mergeRow = section.prependRow(); + mergeRow.join("firstName", "lastName").setText("Name"); + mergeRow.join(mergeRow.getCell("streetAddress"), + mergeRow.getCell("zipCode")); + } + + @Test(expected = IllegalStateException.class) + public void testJoinHeaderCellsIncorrectly() { + final GridHeader section = grid.getHeader(); + HeaderRow mergeRow = section.prependRow(); + mergeRow.join("firstName", "zipCode").setText("Name"); + } + + @Test + public void testJoinAllFooterrCells() { + final GridFooter section = grid.getFooter(); + FooterRow mergeRow = section.prependRow(); + mergeRow.join(dataSource.getContainerPropertyIds().toArray()).setText( + "All the stuff."); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java new file mode 100644 index 0000000000..5583fc02e8 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.Converter.ConversionException; +import com.vaadin.data.util.converter.StringToIntegerConverter; +import com.vaadin.server.VaadinSession; +import com.vaadin.tests.util.AlwaysLockedVaadinSession; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; +import com.vaadin.ui.components.grid.renderers.TextRenderer; + +public class RendererTest { + + private static class TestBean { + int i = 42; + } + + private static class ExtendedBean extends TestBean { + float f = 3.14f; + } + + private static class TestRenderer extends TextRenderer { + @Override + public Object encode(String value) { + return "renderer(" + super.encode(value) + ")"; + } + } + + private static class TestConverter implements Converter<String, TestBean> { + + @Override + public TestBean convertToModel(String value, + Class<? extends TestBean> targetType, Locale locale) + throws ConversionException { + return null; + } + + @Override + public String convertToPresentation(TestBean value, + Class<? extends String> targetType, Locale locale) + throws ConversionException { + if (value instanceof ExtendedBean) { + return "ExtendedBean(" + value.i + ", " + + ((ExtendedBean) value).f + ")"; + } else { + return "TestBean(" + value.i + ")"; + } + } + + @Override + public Class<TestBean> getModelType() { + return TestBean.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + } + + private Grid grid; + + private GridColumn foo; + private GridColumn bar; + private GridColumn baz; + private GridColumn bah; + + @Before + public void setUp() { + VaadinSession.setCurrent(new AlwaysLockedVaadinSession(null)); + + IndexedContainer c = new IndexedContainer(); + + c.addContainerProperty("foo", Integer.class, 0); + c.addContainerProperty("bar", String.class, ""); + c.addContainerProperty("baz", TestBean.class, null); + c.addContainerProperty("bah", ExtendedBean.class, null); + + Object id = c.addItem(); + Item item = c.getItem(id); + item.getItemProperty("foo").setValue(123); + item.getItemProperty("bar").setValue("321"); + item.getItemProperty("baz").setValue(new TestBean()); + item.getItemProperty("bah").setValue(new ExtendedBean()); + + grid = new Grid(c); + + foo = grid.getColumn("foo"); + bar = grid.getColumn("bar"); + baz = grid.getColumn("baz"); + bah = grid.getColumn("bah"); + } + + @Test + public void testDefaultRendererAndConverter() throws Exception { + assertSame(TextRenderer.class, foo.getRenderer().getClass()); + assertSame(StringToIntegerConverter.class, foo.getConverter() + .getClass()); + + assertSame(TextRenderer.class, bar.getRenderer().getClass()); + // String->String; converter not needed + assertNull(bar.getConverter()); + + assertSame(TextRenderer.class, baz.getRenderer().getClass()); + // MyBean->String; converter not found + assertNull(baz.getConverter()); + } + + @Test + public void testFindCompatibleConverter() throws Exception { + foo.setRenderer(renderer()); + assertSame(StringToIntegerConverter.class, foo.getConverter() + .getClass()); + + bar.setRenderer(renderer()); + assertNull(bar.getConverter()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCannotFindConverter() { + baz.setRenderer(renderer()); + } + + @Test + public void testExplicitConverter() throws Exception { + baz.setRenderer(renderer(), converter()); + bah.setRenderer(renderer(), converter()); + } + + @Test + public void testEncoding() throws Exception { + assertEquals("42", render(foo, 42)); + foo.setRenderer(renderer()); + assertEquals("renderer(42)", render(foo, 42)); + + assertEquals("2.72", render(bar, "2.72")); + bar.setRenderer(new TestRenderer()); + assertEquals("renderer(2.72)", render(bar, "2.72")); + } + + @Test(expected = ConversionException.class) + public void testEncodingWithoutConverter() throws Exception { + render(baz, new TestBean()); + } + + @Test + public void testBeanEncoding() throws Exception { + baz.setRenderer(renderer(), converter()); + bah.setRenderer(renderer(), converter()); + + assertEquals("renderer(TestBean(42))", render(baz, new TestBean())); + assertEquals("renderer(ExtendedBean(42, 3.14))", + render(baz, new ExtendedBean())); + + assertEquals("renderer(ExtendedBean(42, 3.14))", + render(bah, new ExtendedBean())); + } + + private TestConverter converter() { + return new TestConverter(); + } + + private TestRenderer renderer() { + return new TestRenderer(); + } + + private Object render(GridColumn column, Object value) { + return RpcDataProviderExtension.encodeValue(value, + column.getRenderer(), column.getConverter(), grid.getLocale()); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java new file mode 100644 index 0000000000..d3a9315e20 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid.sort; + +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.shared.ui.grid.SortDirection; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.SortOrderChangeEvent; +import com.vaadin.ui.components.grid.SortOrderChangeListener; +import com.vaadin.ui.components.grid.sort.Sort; +import com.vaadin.ui.components.grid.sort.SortOrder; + +public class SortTest { + + class DummySortingIndexedContainer extends IndexedContainer { + + private Object[] expectedProperties; + private boolean[] expectedAscending; + private boolean sorted = true; + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + Assert.assertEquals( + "Different amount of expected and actual properties,", + expectedProperties.length, propertyId.length); + Assert.assertEquals( + "Different amount of expected and actual directions", + expectedAscending.length, ascending.length); + for (int i = 0; i < propertyId.length; ++i) { + Assert.assertEquals("Sorting properties differ", + expectedProperties[i], propertyId[i]); + Assert.assertEquals("Sorting directions differ", + expectedAscending[i], ascending[i]); + } + sorted = true; + } + + public void expectedSort(Object[] properties, SortDirection[] directions) { + assert directions.length == properties.length : "Array dimensions differ"; + expectedProperties = properties; + expectedAscending = new boolean[directions.length]; + for (int i = 0; i < directions.length; ++i) { + expectedAscending[i] = (directions[i] == SortDirection.ASCENDING); + } + sorted = false; + } + + public boolean isSorted() { + return sorted; + } + } + + class RegisteringSortChangeListener implements SortOrderChangeListener { + private List<SortOrder> order; + + @Override + public void sortOrderChange(SortOrderChangeEvent event) { + assert order == null : "The same listener was notified multipe times without checking"; + + order = event.getSortOrder(); + } + + public void assertEventFired(SortOrder... expectedOrder) { + Assert.assertEquals(Arrays.asList(expectedOrder), order); + + // Reset for nest test + order = null; + } + + } + + private DummySortingIndexedContainer container; + private RegisteringSortChangeListener listener; + private Grid grid; + + @Before + public void setUp() { + container = createContainer(); + container.expectedSort(new Object[] {}, new SortDirection[] {}); + + listener = new RegisteringSortChangeListener(); + + grid = new Grid(container); + grid.addSortOrderChangeListener(listener); + } + + @After + public void tearDown() { + Assert.assertTrue("Container was not sorted after the test.", + container.isSorted()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidSortDirection() { + Sort.by("foo", null); + } + + @Test(expected = IllegalStateException.class) + public void testSortOneColumnMultipleTimes() { + Sort.by("foo").then("bar").then("foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void testSortingByUnexistingProperty() { + grid.sort("foobar"); + } + + @Test(expected = IllegalArgumentException.class) + public void testSortingByUnsortableProperty() { + container.addContainerProperty("foobar", Object.class, null); + grid.sort("foobar"); + } + + @Test + public void testGridDirectSortAscending() { + container.expectedSort(new Object[] { "foo" }, + new SortDirection[] { SortDirection.ASCENDING }); + grid.sort("foo"); + + listener.assertEventFired(new SortOrder("foo", SortDirection.ASCENDING)); + } + + @Test + public void testGridDirectSortDescending() { + container.expectedSort(new Object[] { "foo" }, + new SortDirection[] { SortDirection.DESCENDING }); + grid.sort("foo", SortDirection.DESCENDING); + + listener.assertEventFired(new SortOrder("foo", SortDirection.DESCENDING)); + } + + @Test + public void testGridSortBy() { + container.expectedSort(new Object[] { "foo", "bar", "baz" }, + new SortDirection[] { SortDirection.ASCENDING, + SortDirection.ASCENDING, SortDirection.DESCENDING }); + grid.sort(Sort.by("foo").then("bar") + .then("baz", SortDirection.DESCENDING)); + + listener.assertEventFired( + new SortOrder("foo", SortDirection.ASCENDING), new SortOrder( + "bar", SortDirection.ASCENDING), new SortOrder("baz", + SortDirection.DESCENDING)); + + } + + @Test + public void testChangeContainerAfterSorting() { + container.expectedSort(new Object[] { "foo", "bar", "baz" }, + new SortDirection[] { SortDirection.ASCENDING, + SortDirection.ASCENDING, SortDirection.DESCENDING }); + grid.sort(Sort.by("foo").then("bar") + .then("baz", SortDirection.DESCENDING)); + + listener.assertEventFired( + new SortOrder("foo", SortDirection.ASCENDING), new SortOrder( + "bar", SortDirection.ASCENDING), new SortOrder("baz", + SortDirection.DESCENDING)); + + container = new DummySortingIndexedContainer(); + container.addContainerProperty("baz", String.class, ""); + container.expectedSort(new Object[] { "baz" }, + new SortDirection[] { SortDirection.DESCENDING }); + grid.setContainerDataSource(container); + + listener.assertEventFired(new SortOrder("baz", SortDirection.DESCENDING)); + + } + + private DummySortingIndexedContainer createContainer() { + DummySortingIndexedContainer container = new DummySortingIndexedContainer(); + container.addContainerProperty("foo", Integer.class, 0); + container.addContainerProperty("bar", Integer.class, 0); + container.addContainerProperty("baz", Integer.class, 0); + return container; + } +} diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java new file mode 100644 index 0000000000..21e299e68b --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.data; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * RPC interface used for pushing container data to the client. + * + * @since + * @author Vaadin Ltd + */ +public interface DataProviderRpc extends ClientRpc { + + /** + * Sends updated row data to a client. + * <p> + * rowDataJson represents a JSON array of JSON objects in the following + * format: + * + * <pre> + * [{ + * "d": [COL_1_JSON, COL_2_json, ...], + * "k": "1" + * }, + * ... + * ] + * </pre> + * + * where COL_INDEX is the index of the column (as a string), and COL_n_JSON + * is valid JSON of the column's data. + * + * @param firstRowIndex + * the index of the first updated row + * @param rowDataJson + * the updated row data + * @see com.vaadin.shared.ui.grid.GridState#JSONKEY_DATA + * @see com.vaadin.ui.components.grid.Renderer#encode(Object) + */ + public void setRowData(int firstRowIndex, String rowDataJson); + + /** + * Informs the client to remove row data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of rows removed from <code>firstRowIndex</code> and + * onwards + */ + public void removeRowData(int firstRowIndex, int count); + + /** + * Informs the client to insert new row data. + * + * @param firstRowIndex + * the index of the first new row + * @param count + * the number of rows inserted at <code>firstRowIndex</code> + */ + public void insertRowData(int firstRowIndex, int count); +} diff --git a/shared/src/com/vaadin/shared/data/DataProviderState.java b/shared/src/com/vaadin/shared/data/DataProviderState.java new file mode 100644 index 0000000000..76d68e8352 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataProviderState.java @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.data; + +import com.vaadin.shared.communication.SharedState; + +/** + * Shared state used by client-side data sources. + * + * @since + * @author Vaadin Ltd + */ +public class DataProviderState extends SharedState { + /** + * The size of the container. + */ + public int containerSize; +} diff --git a/shared/src/com/vaadin/shared/data/DataRequestRpc.java b/shared/src/com/vaadin/shared/data/DataRequestRpc.java new file mode 100644 index 0000000000..000d196be9 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataRequestRpc.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.data; + +import java.util.List; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * RPC interface used for requesting container data to the client. + * + * @since + * @author Vaadin Ltd + */ +public interface DataRequestRpc extends ServerRpc { + + /** + * Request rows from the server. + * + * @param firstRowIndex + * the index of the first requested row + * @param numberOfRows + * the number of requested rows + * @param firstCachedRowIndex + * the index of the first cached row + * @param cacheSize + * the number of cached rows + * @param temporarilyPinnedKeys + * the keys that should remain pinned, even if some of these + * would fall out of the cache range + */ + public void requestRows(int firstRowIndex, int numberOfRows, + int firstCachedRowIndex, int cacheSize, + List<String> temporarilyPinnedKeys); + + /** + * Informs the back-end that the temporarily pinned keys in + * {@link #requestRows(int, int, int, int, List)} may be released. + */ + public void releaseTemporarilyPinnedKeys(); +} diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java new file mode 100644 index 0000000000..2ef0dfc3f8 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * The column group data shared between the server and the client + * + * @since + * @author Vaadin Ltd + */ +public class ColumnGroupState implements Serializable { + + /** + * The columns that is included in the group + */ + public List<String> columns = new ArrayList<String>(); + + /** + * The header text of the group + */ + public String header; + + /** + * The footer text of the group + */ + public String footer; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java new file mode 100644 index 0000000000..ade9e87f36 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * Server-to-client RPC interface for the Grid component. + * + * @since + * @author Vaadin Ltd + */ +public interface GridClientRpc extends ClientRpc { + + /** + * Command client Grid to scroll to a specific data row. + * + * @param row + * zero-based row index. If the row index is below zero or above + * the row count of the client-side data source, a client-side + * exception will be triggered. Since this exception has no + * handling by default, an out-of-bounds value will cause a + * client-side crash. + * @param destination + * desired placement of scrolled-to row. See the documentation + * for {@link ScrollDestination} for more information. + */ + public void scrollToRow(int row, ScrollDestination destination); + + /** + * Command client Grid to scroll to the first row. + */ + public void scrollToStart(); + + /** + * Command client Grid to scroll to the last row. + */ + public void scrollToEnd(); + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java new file mode 100644 index 0000000000..b73e7cffd5 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; + +import com.vaadin.shared.Connector; + +/** + * Column state DTO for transferring column properties from the server to the + * client + * + * @since + * @author Vaadin Ltd + */ +public class GridColumnState implements Serializable { + + /** + * Id used by grid connector to map server side column with client side + * column + */ + public String id; + + /** + * Header caption for the column + */ + @Deprecated + public String header; + + /** + * Footer caption for the column + */ + @Deprecated + public String footer; + + /** + * Has the column been hidden. By default the column is visible. + */ + public boolean visible = true; + + /** + * Column width in pixels. Default column width is 100px. + */ + public int width = 100; + + public Connector rendererConnector; + + /** + * Are sorting indicators shown for a column. Default is false. + */ + public boolean sortable = false; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridConstants.java b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java new file mode 100644 index 0000000000..1ee79a5d37 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; + +/** + * Container class for common constants and default values used by the Grid + * component. + * + * @since + * @author Vaadin Ltd + */ +public final class GridConstants implements Serializable { + + /** + * Default padding in pixels when scrolling programmatically, without an + * explicitly defined padding value. + */ + public static final int DEFAULT_PADDING = 0; + + /** + * Delay before a long tap action is triggered. Number in milliseconds. + */ + public static final int LONG_TAP_DELAY = 500; + + /** + * The threshold in pixels a finger can move while long tapping. + */ + public static final int LONG_TAP_THRESHOLD = 3; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java new file mode 100644 index 0000000000..fd671e30a7 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.util.List; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * Client-to-server RPC interface for the Grid component + * + * @since + * @author Vaadin Ltd + */ +public interface GridServerRpc extends ServerRpc { + void selectionChange(List<String> newSelection); + + void sort(String[] columnIds, SortDirection[] directions, + SortEventOriginator originator); +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java new file mode 100644 index 0000000000..d687dd8e48 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -0,0 +1,130 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.ui.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.annotations.DelegateToWidget; + +/** + * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component + * + * @since + * @author Vaadin Ltd + */ +public class GridState extends AbstractComponentState { + + /** + * A description of which of the three bundled SelectionModels is currently + * in use. + * <p> + * Used as a data transfer object instead of the client/server ones, because + * they don't know about each others classes. + * + * @see com.vaadin.ui.components.grid.Grid.SelectionMode + * @see com.vaadin.client.ui.grid.Grid.SelectionMode + */ + public enum SharedSelectionMode { + /** + * Representation of a single selection mode + * + * @see com.vaadin.ui.components.grid.Grid.SelectionMode#SINGLE + * @see com.vaadin.client.ui.grid.Grid.SelectionMode#SINGLE + */ + SINGLE, + + /** + * Representation of a multiselection mode + * + * @see com.vaadin.ui.components.grid.Grid.SelectionMode#MULTI + * @see com.vaadin.client.ui.grid.Grid.SelectionMode#MULTI + */ + MULTI, + + /** + * Representation of a no-selection mode + * + * @see com.vaadin.ui.components.grid.Grid.SelectionMode#NONE + * @see com.vaadin.client.ui.grid.Grid.SelectionMode#NONE + */ + NONE; + } + + /** + * The default value for height-by-rows for both GWT widgets + * {@link com.vaadin.ui.components.grid Grid} and + * {@link com.vaadin.client.ui.grid.Escalator Escalator} + */ + public static final double DEFAULT_HEIGHT_BY_ROWS = 10.0d; + + /** + * The key in which a row's data can be found + * + * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String) + */ + public static final String JSONKEY_DATA = "d"; + + /** + * The key in which a row's own key can be found + * + * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String) + */ + public static final String JSONKEY_ROWKEY = "k"; + + { + // FIXME Grid currently does not support undefined size + width = "400px"; + height = "400px"; + } + + /** + * Columns in grid. Column order implicitly deferred from list order. + */ + public List<GridColumnState> columns = new ArrayList<GridColumnState>(); + + public GridStaticSectionState header = new GridStaticSectionState(); + + public GridStaticSectionState footer = new GridStaticSectionState(); + + /** + * The id for the last frozen column. + * + * @see GridColumnState#id + */ + public String lastFrozenColumnId = null; + + /** The height of the Grid in terms of body rows. */ + @DelegateToWidget + public double heightByRows = DEFAULT_HEIGHT_BY_ROWS; + + /** The mode by which Grid defines its height. */ + @DelegateToWidget + public HeightMode heightMode = HeightMode.CSS; + + // instantiated just to avoid NPEs + public List<String> selectedKeys = new ArrayList<String>(); + + public SharedSelectionMode selectionMode; + + /** Keys of the currently sorted columns */ + public String[] sortColumns = new String[0]; + + /** Directions for each sorted column */ + public SortDirection[] sortDirs = new SortDirection[0]; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java b/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java new file mode 100644 index 0000000000..eae4bc8da4 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +/** + * Enumeration, specifying the content type of a Cell in a GridStaticSection. + * + * @since + * @author Vaadin Ltd + */ +public enum GridStaticCellType { + /** + * Text content + */ + TEXT, + + /** + * HTML content + */ + HTML, + + /** + * Widget content + */ + WIDGET; +}
\ No newline at end of file diff --git a/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java b/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java new file mode 100644 index 0000000000..c3c373b5af --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.Connector; + +/** + * Shared state for Grid headers and footers. + * + * @since + * @author Vaadin Ltd + */ +public class GridStaticSectionState implements Serializable { + + public static class CellState implements Serializable { + public String text = ""; + + public String html = ""; + + public Connector connector = null; + + public GridStaticCellType type = GridStaticCellType.TEXT; + } + + public static class RowState implements Serializable { + public List<CellState> cells = new ArrayList<CellState>(); + + public boolean defaultRow = false; + + public List<List<Integer>> cellGroups = new ArrayList<List<Integer>>(); + } + + public List<RowState> rows = new ArrayList<RowState>(); + + public boolean visible = true; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/HeightMode.java b/shared/src/com/vaadin/shared/ui/grid/HeightMode.java new file mode 100644 index 0000000000..228fcbf0f4 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/HeightMode.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +/** + * The modes for height calculation that are supported by Grid ( + * {@link com.vaadin.client.ui.grid.Grid client} and + * {@link com.vaadin.ui.components.grid.Grid server}) / + * {@link com.vaadin.client.ui.grid.Escalator Escalator}. + * + * @since + * @author Vaadin Ltd + * @see com.vaadin.client.ui.grid.Grid#setHeightMode(HeightMode) + * @see com.vaadin.ui.components.grid.Grid#setHeightMode(HeightMode) + * @see com.vaadin.client.ui.grid.Escalator#setHeightMode(HeightMode) + */ +public enum HeightMode { + /** + * The height of the Component or Widget is defined by a CSS-like value. + * (e.g. "100px", "50em" or "25%") + */ + CSS, + + /** + * The height of the Component or Widget in question is defined by a number + * of rows. + */ + ROW; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java new file mode 100644 index 0000000000..2054845320 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/Range.java @@ -0,0 +1,431 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; + +/** + * An immutable representation of a range, marked by start and end points. + * <p> + * The range is treated as inclusive at the start, and exclusive at the end. + * I.e. the range [0..1[ has the length 1, and represents one integer: 0. + * <p> + * The range is considered {@link #isEmpty() empty} if the start is the same as + * the end. + * + * @since + * @author Vaadin Ltd + */ +public final class Range implements Serializable { + private final int start; + private final int end; + + /** + * Creates a range object representing a single integer. + * + * @param integer + * the number to represent as a range + * @return the range represented by <code>integer</code> + */ + public static Range withOnly(final int integer) { + return new Range(integer, integer + 1); + } + + /** + * Creates a range between two integers. + * <p> + * The range start is <em>inclusive</em> and the end is <em>exclusive</em>. + * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, + * but not 5. + * + * @param start + * the start of the the range, inclusive + * @param end + * the end of the range, exclusive + * @return a range representing <code>[start..end[</code> + * @throws IllegalArgumentException + * if <code>start > end</code> + */ + public static Range between(final int start, final int end) + throws IllegalArgumentException { + return new Range(start, end); + } + + /** + * Creates a range from a start point, with a given length. + * + * @param start + * the first integer to include in the range + * @param length + * the length of the resulting range + * @return a range starting from <code>start</code>, with + * <code>length</code> number of integers following + * @throws IllegalArgumentException + * if length < 0 + */ + public static Range withLength(final int start, final int length) + throws IllegalArgumentException { + if (length < 0) { + /* + * The constructor of Range will throw an exception if start > + * start+length (i.e. if length is negative). We're throwing the + * same exception type, just with a more descriptive message. + */ + throw new IllegalArgumentException("length must not be negative"); + } + return new Range(start, start + length); + } + + /** + * Creates a new range between two numbers: <code>[start..end[</code>. + * + * @param start + * the start integer, inclusive + * @param end + * the end integer, exclusive + * @throws IllegalArgumentException + * if <code>start > end</code> + */ + private Range(final int start, final int end) + throws IllegalArgumentException { + if (start > end) { + throw new IllegalArgumentException( + "start must not be greater than end"); + } + + this.start = start; + this.end = end; + } + + /** + * Returns the <em>inclusive</em> start point of this range. + * + * @return the start point of this range + */ + public int getStart() { + return start; + } + + /** + * Returns the <em>exclusive</em> end point of this range. + * + * @return the end point of this range + */ + public int getEnd() { + return end; + } + + /** + * The number of integers contained in the range. + * + * @return the number of integers contained in the range + */ + public int length() { + return getEnd() - getStart(); + } + + /** + * Checks whether the range has no elements between the start and end. + * + * @return <code>true</code> iff the range contains no elements. + */ + public boolean isEmpty() { + return getStart() >= getEnd(); + } + + /** + * Checks whether this range and another range are at least partially + * covering the same values. + * + * @param other + * the other range to check against + * @return <code>true</code> if this and <code>other</code> intersect + */ + public boolean intersects(final Range other) { + return getStart() < other.getEnd() && other.getStart() < getEnd(); + } + + /** + * Checks whether an integer is found within this range. + * + * @param integer + * an integer to test for presence in this range + * @return <code>true</code> iff <code>integer</code> is in this range + */ + public boolean contains(final int integer) { + return getStart() <= integer && integer < getEnd(); + } + + /** + * Checks whether this range is a subset of another range. + * + * @return <code>true</code> iff <code>other</code> completely wraps this + * range + */ + public boolean isSubsetOf(final Range other) { + return other.getStart() <= getStart() && getEnd() <= other.getEnd(); + } + + /** + * Overlay this range with another one, and partition the ranges according + * to how they position relative to each other. + * <p> + * The three partitions are returned as a three-element Range array: + * <ul> + * <li>Elements in this range that occur before elements in + * <code>other</code>. + * <li>Elements that are shared between the two ranges. + * <li>Elements in this range that occur after elements in + * <code>other</code>. + * </ul> + * + * @param other + * the other range to act as delimiters. + * @return a three-element Range array of partitions depicting the elements + * before (index 0), shared/inside (index 1) and after (index 2). + */ + public Range[] partitionWith(final Range other) { + final Range[] splitBefore = splitAt(other.getStart()); + final Range rangeBefore = splitBefore[0]; + final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd()); + final Range rangeInside = splitAfter[0]; + final Range rangeAfter = splitAfter[1]; + return new Range[] { rangeBefore, rangeInside, rangeAfter }; + } + + /** + * Get a range that is based on this one, but offset by a number. + * + * @param offset + * the number to offset by + * @return a copy of this range, offset by <code>offset</code> + */ + public Range offsetBy(final int offset) { + if (offset == 0) { + return this; + } else { + return new Range(start + offset, end + offset); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd() + + "[" + (isEmpty() ? " (empty)" : ""); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + end; + result = prime * result + start; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Range other = (Range) obj; + if (end != other.end) { + return false; + } + if (start != other.start) { + return false; + } + return true; + } + + /** + * Checks whether this range starts before the start of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range starts before the + * <code>other</code> + */ + public boolean startsBefore(final Range other) { + return getStart() < other.getStart(); + } + + /** + * Checks whether this range ends before the start of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range ends before the + * <code>other</code> + */ + public boolean endsBefore(final Range other) { + return getEnd() <= other.getStart(); + } + + /** + * Checks whether this range ends after the end of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range ends after the + * <code>other</code> + */ + public boolean endsAfter(final Range other) { + return getEnd() > other.getEnd(); + } + + /** + * Checks whether this range starts after the end of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range starts after the + * <code>other</code> + */ + public boolean startsAfter(final Range other) { + return getStart() >= other.getEnd(); + } + + /** + * Split the range into two at a certain integer. + * <p> + * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code> + * + * @param integer + * the integer at which to split the range into two + * @return an array of two ranges, with <code>[start..integer[</code> in the + * first element, and <code>[integer..end[</code> in the second + * element. + * <p> + * If {@code integer} is less than {@code start}, [empty, + * {@code this} ] is returned. if <code>integer</code> is equal to + * or greater than {@code end}, [{@code this}, empty] is returned + * instead. + */ + public Range[] splitAt(final int integer) { + if (integer < start) { + return new Range[] { Range.withLength(start, 0), this }; + } else if (integer >= end) { + return new Range[] { this, Range.withLength(end, 0) }; + } else { + return new Range[] { new Range(start, integer), + new Range(integer, end) }; + } + } + + /** + * Split the range into two after a certain number of integers into the + * range. + * <p> + * Calling this method is equivalent to calling + * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code> + * <p> + * <em>Example:</em> + * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code> + * + * @param length + * the length at which to split this range into two + * @return an array of two ranges, having the <code>length</code>-first + * elements of this range, and the second range having the rest. If + * <code>length</code> ≤ 0, the first element will be empty, and + * the second element will be this range. If <code>length</code> + * ≥ {@link #length()}, the first element will be this range, + * and the second element will be empty. + */ + public Range[] splitAtFromStart(final int length) { + return splitAt(getStart() + length); + } + + /** + * Combines two ranges to create a range containing all values in both + * ranges, provided there are no gaps between the ranges. + * + * @param other + * the range to combine with this range + * + * @return the combined range + * + * @throws IllegalArgumentException + * if the two ranges aren't connected + */ + public Range combineWith(Range other) throws IllegalArgumentException { + if (getStart() > other.getEnd() || other.getStart() > getEnd()) { + throw new IllegalArgumentException("There is a gap between " + this + + " and " + other); + } + + return Range.between(Math.min(getStart(), other.getStart()), + Math.max(getEnd(), other.getEnd())); + } + + /** + * Creates a range that is expanded the given amounts in both ends. + * + * @param startDelta + * the amount to expand by in the beginning of the range + * @param endDelta + * the amount to expand by in the end of the range + * + * @return an expanded range + * + * @throws IllegalArgumentException + * if the new range would have <code>start > end</code> + */ + public Range expand(int startDelta, int endDelta) + throws IllegalArgumentException { + return Range.between(getStart() - startDelta, getEnd() + endDelta); + } + + /** + * Limits this range to be within the bounds of the provided range. + * <p> + * This is basically an optimized way of calculating + * <code>{@link #partitionWith(Range)}[1]</code> without the overhead of + * defining the parts that do not overlap. + * <p> + * If the two ranges do not intersect, an empty range is returned. There are + * no guarantees about the position of that range. + * + * @param bounds + * the bounds that the returned range should be limited to + * @return a bounded range + */ + public Range restrictTo(Range bounds) { + boolean startWithin = getStart() >= bounds.getStart(); + boolean endWithin = getEnd() <= bounds.getEnd(); + + if (startWithin) { + if (endWithin) { + return this; + } else { + return Range.between(getStart(), bounds.getEnd()); + } + } else { + if (endWithin) { + return Range.between(bounds.getStart(), getEnd()); + } else { + return bounds; + } + } + } +} diff --git a/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java new file mode 100644 index 0000000000..43d5fcc21b --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +/** + * Enumeration, specifying the destinations that are supported when scrolling + * rows or columns into view. + * + * @since + * @author Vaadin Ltd + */ +public enum ScrollDestination { + + /** + * Scroll as little as possible to show the target element. If the element + * fits into view, this works as START or END depending on the current + * scroll position. If the element does not fit into view, this works as + * START. + */ + ANY, + + /** + * Scrolls so that the element is shown at the start of the viewport. The + * viewport will, however, not scroll beyond its contents. + */ + START, + + /** + * Scrolls so that the element is shown in the middle of the viewport. The + * viewport will, however, not scroll beyond its contents, given more + * elements than what the viewport is able to show at once. Under no + * circumstances will the viewport scroll before its first element. + */ + MIDDLE, + + /** + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + END + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/SortDirection.java b/shared/src/com/vaadin/shared/ui/grid/SortDirection.java new file mode 100644 index 0000000000..0b4eafc37f --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/SortDirection.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; + +/** + * Describes sorting direction for a Grid column + * + * @since + * @author Vaadin Ltd + */ +public enum SortDirection implements Serializable { + + /** + * Ascending (e.g. A-Z, 1..9) sort order + */ + ASCENDING, + + /** + * Descending (e.g. Z-A, 9..1) sort order + */ + DESCENDING +} diff --git a/shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java b/shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java new file mode 100644 index 0000000000..acdd46ea5b --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +/** + * Identifier for the originator of a sort event or sort order change event. + * + * @since + * @author Vaadin Ltd + */ +public enum SortEventOriginator { + + /** + * This event was the result of an API call. + */ + API, + + /** + * This event was the result of a user interacting with the UI. + */ + USER, + + /** + * This event resulted as a side-effect of an internal event. + */ + INTERNAL + +} diff --git a/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java new file mode 100644 index 0000000000..ab67b22d0b --- /dev/null +++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java @@ -0,0 +1,406 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +@SuppressWarnings("static-method") +public class RangeTest { + + @Test(expected = IllegalArgumentException.class) + public void startAfterEndTest() { + Range.between(10, 9); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeLengthTest() { + Range.withLength(10, -1); + } + + @Test + public void constructorEquivalenceTest() { + assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11)); + assertEquals("[10,20[ == 10, length 10", Range.between(10, 20), + Range.withLength(10, 10)); + assertEquals("10 == 10, length 1", Range.withOnly(10), + Range.withLength(10, 1)); + } + + @Test + public void boundsTest() { + { + final Range range = Range.between(0, 10); + assertEquals("between(0, 10) start", 0, range.getStart()); + assertEquals("between(0, 10) end", 10, range.getEnd()); + } + + { + final Range single = Range.withOnly(10); + assertEquals("withOnly(10) start", 10, single.getStart()); + assertEquals("withOnly(10) end", 11, single.getEnd()); + } + + { + final Range length = Range.withLength(10, 5); + assertEquals("withLength(10, 5) start", 10, length.getStart()); + assertEquals("withLength(10, 5) end", 15, length.getEnd()); + } + } + + @Test + @SuppressWarnings("boxing") + public void equalsTest() { + final Range range1 = Range.between(0, 10); + final Range range2 = Range.withLength(0, 11); + + assertTrue("null", !range1.equals(null)); + assertTrue("reflexive", range1.equals(range1)); + assertEquals("symmetric", range1.equals(range2), range2.equals(range1)); + } + + @Test + public void containsTest() { + final int start = 0; + final int end = 10; + final Range range = Range.between(start, end); + + assertTrue("start should be contained", range.contains(start)); + assertTrue("start-1 should not be contained", + !range.contains(start - 1)); + assertTrue("end should not be contained", !range.contains(end)); + assertTrue("end-1 should be contained", range.contains(end - 1)); + + assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5)); + assertTrue("empty range does not contain 5", !Range.between(5, 5) + .contains(5)); + } + + @Test + public void emptyTest() { + assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty()); + assertTrue("Range of length 0 should be empty", Range.withLength(0, 0) + .isEmpty()); + + assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty()); + assertTrue("Range of length 1 should not be empty", + !Range.withLength(0, 1).isEmpty()); + } + + @Test + public void splitTest() { + final Range startRange = Range.between(0, 10); + final Range[] splitRanges = startRange.splitAt(5); + assertEquals("[0..10[ split at 5, lower", Range.between(0, 5), + splitRanges[0]); + assertEquals("[0..10[ split at 5, upper", Range.between(5, 10), + splitRanges[1]); + } + + @Test + public void split_valueBefore() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(5); + + assertEquals(Range.between(10, 10), splitRanges[0]); + assertEquals(range, splitRanges[1]); + } + + @Test + public void split_valueAfter() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(25); + + assertEquals(range, splitRanges[0]); + assertEquals(Range.between(20, 20), splitRanges[1]); + } + + @Test + public void emptySplitTest() { + final Range range = Range.between(5, 10); + final Range[] split1 = range.splitAt(0); + assertTrue("split1, [0]", split1[0].isEmpty()); + assertEquals("split1, [1]", range, split1[1]); + + final Range[] split2 = range.splitAt(15); + assertEquals("split2, [0]", range, split2[0]); + assertTrue("split2, [1]", split2[1].isEmpty()); + } + + @Test + public void lengthTest() { + assertEquals("withLength length", 5, Range.withLength(10, 5).length()); + assertEquals("between length", 5, Range.between(10, 15).length()); + assertEquals("withOnly 10 length", 1, Range.withOnly(10).length()); + } + + @Test + public void intersectsTest() { + assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10) + .intersects(Range.between(5, 15))); + assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10) + .intersects(Range.between(10, 20))); + } + + @Test + public void intersects_emptyInside() { + assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5) + .intersects(Range.between(0, 10))); + assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10) + .intersects(Range.between(5, 5))); + } + + @Test + public void intersects_emptyOutside() { + assertTrue("[15..15[ does not intersect with [0..10[", + !Range.between(15, 15).intersects(Range.between(0, 10))); + assertTrue("[0..10[ does not intersect with [15..15[", + !Range.between(0, 10).intersects(Range.between(15, 15))); + } + + @Test + public void subsetTest() { + assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10) + .isSubsetOf(Range.between(0, 20))); + + final Range range = Range.between(0, 10); + assertTrue("range is subset of self", range.isSubsetOf(range)); + + assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10) + .isSubsetOf(Range.between(5, 15))); + } + + @Test + public void offsetTest() { + assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5)); + } + + @Test + public void rangeStartsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(1, 5); + assertTrue("former should starts before latter", + former.startsBefore(latter)); + assertTrue("latter shouldn't start before latter", + !latter.startsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(0, 5).startsBefore(Range.between(0, 10))); + } + + @Test + public void rangeStartsAfterTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should start after former", + latter.startsAfter(former)); + assertTrue("former shouldn't start after latter", + !former.startsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).startsAfter(Range.between(0, 6))); + } + + @Test + public void rangeEndsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should end before former", former.endsBefore(latter)); + assertTrue("former shouldn't end before latter", + !latter.endsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).endsBefore(Range.between(9, 15))); + } + + @Test + public void rangeEndsAfterTest() { + final Range former = Range.between(1, 5); + final Range latter = Range.between(1, 6); + assertTrue("latter should end after former", latter.endsAfter(former)); + assertTrue("former shouldn't end after latter", + !former.endsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(0, 10).endsAfter(Range.between(5, 10))); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingFirstSmaller() { + Range.between(0, 10).combineWith(Range.between(11, 20)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingSecondLarger() { + Range.between(11, 20).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_firstEmptyNotOverlapping() { + Range.between(15, 15).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_secondEmptyNotOverlapping() { + Range.between(0, 10).combineWith(Range.between(15, 15)); + } + + @Test + public void combine_barelyOverlapping() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(10, 20); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(20, combined1.getEnd()); + } + + @Test + public void combine_subRange() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(2, 8); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + + @Test + public void combine_intersecting() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 15); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(15, combined1.getEnd()); + + } + + @Test + public void combine_emptyInside() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 5); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + + @Test + public void expand_basic() { + Range r1 = Range.between(5, 10); + Range r2 = r1.expand(2, 3); + + assertEquals(Range.between(3, 13), r2); + } + + @Test + public void expand_negativeLegal() { + Range r1 = Range.between(5, 10); + + Range r2 = r1.expand(-2, -2); + assertEquals(Range.between(7, 8), r2); + + Range r3 = r1.expand(-3, -2); + assertEquals(Range.between(8, 8), r3); + + Range r4 = r1.expand(3, -8); + assertEquals(Range.between(2, 2), r4); + } + + @Test(expected = IllegalArgumentException.class) + public void expand_negativeIllegal1() { + Range r1 = Range.between(5, 10); + + // Should throw because the start would contract beyond the end + r1.expand(-3, -3); + + } + + @Test(expected = IllegalArgumentException.class) + public void expand_negativeIllegal2() { + Range r1 = Range.between(5, 10); + + // Should throw because the end would contract beyond the start + r1.expand(3, -9); + } + + @Test + public void restrictTo_fullyInside() { + Range r1 = Range.between(5, 10); + Range r2 = Range.between(4, 11); + + Range r3 = r1.restrictTo(r2); + assertTrue(r1 == r3); + } + + @Test + public void restrictTo_fullyOutside() { + Range r1 = Range.between(4, 11); + Range r2 = Range.between(5, 10); + + Range r3 = r1.restrictTo(r2); + assertTrue(r2 == r3); + } + + public void restrictTo_notInterstecting() { + Range r1 = Range.between(5, 10); + Range r2 = Range.between(15, 20); + + Range r3 = r1.restrictTo(r2); + assertTrue("Non-intersecting ranges should produce an empty result", + r3.isEmpty()); + + Range r4 = r2.restrictTo(r1); + assertTrue("Non-intersecting ranges should produce an empty result", + r4.isEmpty()); + } + + public void restrictTo_startOutside() { + Range r1 = Range.between(5, 10); + Range r2 = Range.between(7, 15); + + Range r3 = r1.restrictTo(r2); + + assertEquals(Range.between(7, 10), r3); + } + + public void restrictTo_endOutside() { + Range r1 = Range.between(5, 10); + Range r2 = Range.between(4, 7); + + Range r3 = r1.restrictTo(r2); + + assertEquals(Range.between(5, 7), r3); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java new file mode 100644 index 0000000000..f7af6a57e5 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java @@ -0,0 +1,319 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.tests.components.grid; + +import java.util.Random; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.tests.widgetset.server.grid.TestGrid; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Layout; +import com.vaadin.ui.NativeSelect; +import com.vaadin.ui.TextField; + +@Widgetset(TestingWidgetSet.NAME) +public class BasicEscalator extends AbstractTestUI { + public static final String ESCALATOR = "escalator"; + + public static final String INSERT_ROWS_OFFSET = "iro"; + public static final String INSERT_ROWS_AMOUNT = "ira"; + public static final String INSERT_ROWS_BUTTON = "irb"; + + public static final String REMOVE_ROWS_OFFSET = "rro"; + public static final String REMOVE_ROWS_AMOUNT = "rra"; + public static final String REMOVE_ROWS_BUTTON = "rrb"; + + private final Random random = new Random(); + + @Override + protected void setup(final VaadinRequest request) { + final TestGrid grid = new TestGrid(); + grid.setId(ESCALATOR); + addComponent(grid); + + final Layout insertRowsLayout = new HorizontalLayout(); + final TextField insertRowsOffset = new TextField(); + insertRowsOffset.setId(INSERT_ROWS_OFFSET); + insertRowsLayout.addComponent(insertRowsOffset); + final TextField insertRowsAmount = new TextField(); + insertRowsAmount.setId(INSERT_ROWS_AMOUNT); + insertRowsLayout.addComponent(insertRowsAmount); + insertRowsLayout.addComponent(new Button("insert rows", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(insertRowsOffset + .getValue()); + final int amount = Integer.parseInt(insertRowsAmount + .getValue()); + grid.insertRows(offset, amount); + } + }) { + { + setId(INSERT_ROWS_BUTTON); + } + }); + addComponent(insertRowsLayout); + + final Layout removeRowsLayout = new HorizontalLayout(); + final TextField removeRowsOffset = new TextField(); + removeRowsOffset.setId(REMOVE_ROWS_OFFSET); + removeRowsLayout.addComponent(removeRowsOffset); + final TextField removeRowsAmount = new TextField(); + removeRowsAmount.setId(REMOVE_ROWS_AMOUNT); + removeRowsLayout.addComponent(removeRowsAmount); + removeRowsLayout.addComponent(new Button("remove rows", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(removeRowsOffset + .getValue()); + final int amount = Integer.parseInt(removeRowsAmount + .getValue()); + grid.removeRows(offset, amount); + } + }) { + { + setId(REMOVE_ROWS_BUTTON); + } + }); + addComponent(removeRowsLayout); + + final Layout insertColumnsLayout = new HorizontalLayout(); + final TextField insertColumnsOffset = new TextField(); + insertColumnsLayout.addComponent(insertColumnsOffset); + final TextField insertColumnsAmount = new TextField(); + insertColumnsLayout.addComponent(insertColumnsAmount); + insertColumnsLayout.addComponent(new Button("insert columns", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(insertColumnsOffset + .getValue()); + final int amount = Integer.parseInt(insertColumnsAmount + .getValue()); + grid.insertColumns(offset, amount); + } + })); + addComponent(insertColumnsLayout); + + final Layout removeColumnsLayout = new HorizontalLayout(); + final TextField removeColumnsOffset = new TextField(); + removeColumnsLayout.addComponent(removeColumnsOffset); + final TextField removeColumnsAmount = new TextField(); + removeColumnsLayout.addComponent(removeColumnsAmount); + removeColumnsLayout.addComponent(new Button("remove columns", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(removeColumnsOffset + .getValue()); + final int amount = Integer.parseInt(removeColumnsAmount + .getValue()); + grid.removeColumns(offset, amount); + } + })); + addComponent(removeColumnsLayout); + + final HorizontalLayout rowScroll = new HorizontalLayout(); + final NativeSelect destination = new NativeSelect(); + destination.setNullSelectionAllowed(false); + destination.addItem("any"); + destination.setValue("any"); + destination.addItem("start"); + destination.addItem("end"); + destination.addItem("middle"); + rowScroll.addComponent(destination); + final TextField rowIndex = new TextField(); + rowScroll.addComponent(rowIndex); + final TextField rowPadding = new TextField(); + rowScroll.addComponent(rowPadding); + rowScroll.addComponent(new Button("scroll to row", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + int index; + try { + index = Integer.parseInt(rowIndex.getValue()); + } catch (NumberFormatException e) { + index = 0; + } + + int padding; + try { + padding = Integer.parseInt(rowPadding.getValue()); + } catch (NumberFormatException e) { + padding = 0; + } + + grid.scrollToRow(index, + (String) destination.getValue(), padding); + } + })); + addComponent(rowScroll); + + final HorizontalLayout colScroll = new HorizontalLayout(); + final NativeSelect colDestination = new NativeSelect(); + colDestination.setNullSelectionAllowed(false); + colDestination.addItem("any"); + colDestination.setValue("any"); + colDestination.addItem("start"); + colDestination.addItem("end"); + colDestination.addItem("middle"); + colScroll.addComponent(colDestination); + final TextField colIndex = new TextField(); + colScroll.addComponent(colIndex); + final TextField colPadding = new TextField(); + colScroll.addComponent(colPadding); + colScroll.addComponent(new Button("scroll to column", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + int index; + try { + index = Integer.parseInt(colIndex.getValue()); + } catch (NumberFormatException e) { + index = 0; + } + + int padding; + try { + padding = Integer.parseInt(colPadding.getValue()); + } catch (NumberFormatException e) { + padding = 0; + } + + grid.scrollToColumn(index, + (String) colDestination.getValue(), padding); + } + })); + addComponent(colScroll); + + final TextField freezeCount = new TextField(); + freezeCount.setConverter(Integer.class); + freezeCount.setNullRepresentation(""); + addComponent(new HorizontalLayout(freezeCount, new Button( + "set frozen columns", new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.setFrozenColumns(((Integer) freezeCount + .getConvertedValue()).intValue()); + freezeCount.setValue(null); + } + }))); + + addComponent(new Button("Resize randomly", new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + int width = random.nextInt(300) + 500; + int height = random.nextInt(300) + 200; + grid.setWidth(width + "px"); + grid.setHeight(height + "px"); + } + })); + + addComponent(new Button("Random headers count", + new Button.ClickListener() { + private int headers = 1; + + @Override + public void buttonClick(ClickEvent event) { + int diff = 0; + while (diff == 0) { + final int nextHeaders = random.nextInt(4); + diff = nextHeaders - headers; + headers = nextHeaders; + } + if (diff > 0) { + grid.insertHeaders(0, diff); + } else if (diff < 0) { + grid.removeHeaders(0, -diff); + } + } + })); + + addComponent(new Button("Random footers count", + new Button.ClickListener() { + private int footers = 1; + + @Override + public void buttonClick(ClickEvent event) { + int diff = 0; + while (diff == 0) { + final int nextFooters = random.nextInt(4); + diff = nextFooters - footers; + footers = nextFooters; + } + if (diff > 0) { + grid.insertFooters(0, diff); + } else if (diff < 0) { + grid.removeFooters(0, -diff); + } + } + })); + + final Layout resizeColumnsLayout = new HorizontalLayout(); + final TextField resizeColumnIndex = new TextField(); + resizeColumnsLayout.addComponent(resizeColumnIndex); + final TextField resizeColumnPx = new TextField(); + resizeColumnsLayout.addComponent(resizeColumnPx); + resizeColumnsLayout.addComponent(new Button("resize column", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int index = Integer.parseInt(resizeColumnIndex + .getValue()); + final int px = Integer.parseInt(resizeColumnPx + .getValue()); + grid.setColumnWidth(index, px); + } + })); + addComponent(resizeColumnsLayout); + + addComponent(new Button("Autoresize columns", + new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.calculateColumnWidths(); + } + })); + + addComponent(new Button("Randomize row heights", + new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.randomizeDefaultRowHeight(); + } + })); + } + + @Override + protected String getTestDescription() { + return null; + } + + @Override + protected Integer getTicketNumber() { + return null; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java b/uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java new file mode 100644 index 0000000000..ba0b718f35 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java @@ -0,0 +1,295 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.Keys; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import com.vaadin.tests.annotations.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; + +@TestCategory("grid") +public class BasicEscalatorTest extends MultiBrowserTest { + + private static final int SLEEP = 300; + + private static final Pattern ROW_PATTERN = Pattern + .compile("Row (\\d+): \\d+,\\d+"); + + @Test + public void testInitialState() throws Exception { + openTestURL(); + + WebElement cell1 = getBodyRowCell(0, 0); + assertEquals("Top left body cell had unexpected content", "Row 0: 0,0", + cell1.getText()); + + WebElement cell2 = getBodyRowCell(15, 3); + assertEquals("Lower merged cell had unexpected content", "Cell: 3,15", + cell2.getText()); + } + + @Test + public void testScroll() throws Exception { + openTestURL(); + + /* + * let the DOM stabilize itself. TODO: remove once waitForVaadin + * supports lazy loaded components + */ + Thread.sleep(100); + + setScrollTop(getVerticalScrollbar(), 1000); + assertBodyCellWithContentIsFound("Row 50: 0,50"); + } + + @Test + public void testLastRow() throws Exception { + openTestURL(); + + /* + * let the DOM stabilize itself. TODO: remove once waitForVaadin + * supports lazy loaded components + */ + Thread.sleep(100); + + // scroll to bottom + setScrollTop(getVerticalScrollbar(), 100000000); + + /* + * this test does not test DOM reordering, therefore we don't rely on + * child indices - we simply seek by content. + */ + assertBodyCellWithContentIsFound("Row 99: 0,99"); + } + + @Test + public void testNormalRowHeight() throws Exception { + /* + * This is tested with screenshots instead of CSS queries, since some + * browsers report dimensions differently from each other, which is + * uninteresting for our purposes + */ + openTestURL(); + compareScreen("normalHeight"); + } + + @Test + public void testModifiedRowHeight() throws Exception { + /* + * This is tested with screenshots instead of CSS queries, since some + * browsers report dimensions differently from each other, which is + * uninteresting for our purposes + */ + openTestURLWithTheme("reindeer-tests"); + compareScreen("modifiedHeight"); + } + + private void assertBodyCellWithContentIsFound(String cellContent) { + String xpath = "//tbody/tr/td[.='" + cellContent + "']"; + try { + assertNotNull("received a null element with \"" + xpath + "\"", + getDriver().findElement(By.xpath(xpath))); + } catch (NoSuchElementException e) { + fail("Could not find '" + xpath + "'"); + } + } + + private WebElement getBodyRowCell(int row, int col) { + return getDriver().findElement( + By.xpath("//tbody/tr[@class='v-escalator-row'][" + (row + 1) + + "]/td[" + (col + 1) + "]")); + } + + private void openTestURLWithTheme(String themeName) { + String testUrl = getTestUrl(); + testUrl += (testUrl.contains("?")) ? "&" : "?"; + testUrl += "theme=" + themeName; + getDriver().get(testUrl); + } + + private Object executeScript(String script, WebElement element) { + @SuppressWarnings("hiding") + final WebDriver driver = getDriver(); + if (driver instanceof JavascriptExecutor) { + final JavascriptExecutor je = (JavascriptExecutor) driver; + return je.executeScript(script, element); + } else { + throw new IllegalStateException("current driver " + + getDriver().getClass().getName() + " is not a " + + JavascriptExecutor.class.getSimpleName()); + } + } + + @Test + public void domIsInitiallySorted() throws Exception { + openTestURL(); + + final List<WebElement> rows = getBodyRows(); + assertTrue("no body rows found", !rows.isEmpty()); + for (int i = 0; i < rows.size(); i++) { + String text = rows.get(i).getText(); + String expected = "Row " + i; + assertTrue("Expected \"" + expected + "...\" but was " + text, + text.startsWith(expected)); + } + } + + @Test + public void domIsSortedAfterInsert() throws Exception { + openTestURL(); + + final int rowsToInsert = 5; + final int offset = 5; + insertRows(offset, rowsToInsert); + + final List<WebElement> rows = getBodyRows(); + int i = 0; + for (; i < offset + rowsToInsert; i++) { + final String expectedStart = "Row " + i; + final String text = rows.get(i).getText(); + assertTrue("Expected \"" + expectedStart + "...\" but was " + text, + text.startsWith(expectedStart)); + } + + for (; i < rows.size(); i++) { + final String expectedStart = "Row " + (i - rowsToInsert); + final String text = rows.get(i).getText(); + assertTrue("(post insert) Expected \"" + expectedStart + + "...\" but was " + text, text.startsWith(expectedStart)); + } + } + + @Test + public void domIsSortedAfterRemove() throws Exception { + openTestURL(); + + final int rowsToRemove = 5; + final int offset = 5; + removeRows(offset, rowsToRemove); + + final List<WebElement> rows = getBodyRows(); + int i = 0; + for (; i < offset; i++) { + final String expectedStart = "Row " + i; + final String text = rows.get(i).getText(); + assertTrue("Expected " + expectedStart + "... but was " + text, + text.startsWith(expectedStart)); + } + + /* + * We check only up to 10, since after that, the indices are again + * reset, because new rows have been generated. The row numbers that + * they are given depends on the widget size, and it's too fragile to + * rely on some special assumptions on that. + */ + for (; i < 10; i++) { + final String expectedStart = "Row " + (i + rowsToRemove); + final String text = rows.get(i).getText(); + assertTrue("(post remove) Expected " + expectedStart + + "... but was " + text, text.startsWith(expectedStart)); + } + } + + @Test + public void domIsSortedAfterScroll() throws Exception { + openTestURL(); + setScrollTop(getVerticalScrollbar(), 500); + + /* + * Let the DOM reorder itself. + * + * TODO TestBench currently doesn't know when Grid's DOM structure is + * stable. There are some plans regarding implementing support for this, + * so this test case can (should) be modified once that's implemented. + */ + sleep(SLEEP); + + List<WebElement> rows = getBodyRows(); + int firstRowNumber = parseFirstRowNumber(rows); + + for (int i = 0; i < rows.size(); i++) { + final String expectedStart = "Row " + (i + firstRowNumber); + final String text = rows.get(i).getText(); + assertTrue("(post remove) Expected " + expectedStart + + "... but was " + text, text.startsWith(expectedStart)); + } + } + + private static int parseFirstRowNumber(List<WebElement> rows) + throws NumberFormatException { + final WebElement firstRow = rows.get(0); + final String firstRowText = firstRow.getText(); + final Matcher matcher = ROW_PATTERN.matcher(firstRowText); + if (!matcher.find()) { + fail("could not find " + ROW_PATTERN.pattern() + " in \"" + + firstRowText + "\""); + } + final String number = matcher.group(1); + return Integer.parseInt(number); + } + + private void insertRows(final int offset, final int amount) { + final WebElement offsetInput = vaadinElementById(BasicEscalator.INSERT_ROWS_OFFSET); + offsetInput.sendKeys(String.valueOf(offset), Keys.RETURN); + + final WebElement amountInput = vaadinElementById(BasicEscalator.INSERT_ROWS_AMOUNT); + amountInput.sendKeys(String.valueOf(amount), Keys.RETURN); + + final WebElement button = vaadinElementById(BasicEscalator.INSERT_ROWS_BUTTON); + button.click(); + } + + private void removeRows(final int offset, final int amount) { + final WebElement offsetInput = vaadinElementById(BasicEscalator.REMOVE_ROWS_OFFSET); + offsetInput.sendKeys(String.valueOf(offset), Keys.RETURN); + + final WebElement amountInput = vaadinElementById(BasicEscalator.REMOVE_ROWS_AMOUNT); + amountInput.sendKeys(String.valueOf(amount), Keys.RETURN); + + final WebElement button = vaadinElementById(BasicEscalator.REMOVE_ROWS_BUTTON); + button.click(); + } + + private void setScrollTop(WebElement element, long px) { + executeScript("arguments[0].scrollTop = " + px, element); + } + + private List<WebElement> getBodyRows() { + return getDriver().findElements(By.xpath("//tbody/tr/td[1]")); + } + + private WebElement getVerticalScrollbar() { + return getDriver().findElement( + By.xpath("//div[" + + "contains(@class, 'v-escalator-scroller-vertical')" + + "]")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java b/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java new file mode 100644 index 0000000000..d217829bcb --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.ui.Label; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.Grid.SelectionMode; + +@Widgetset(TestingWidgetSet.NAME) +public class CustomRenderer extends AbstractTestUI { + + private static final Object INT_ARRAY_PROPERTY = "int array"; + private static final Object VOID_PROPERTY = "void"; + + static final Object ITEM_ID = "itemId1"; + static final String DEBUG_LABEL_ID = "debuglabel"; + static final String INIT_DEBUG_LABEL_CAPTION = "Debug label placeholder"; + + @Override + protected void setup(VaadinRequest request) { + IndexedContainer container = new IndexedContainer(); + container.addContainerProperty(INT_ARRAY_PROPERTY, int[].class, + new int[] {}); + container.addContainerProperty(VOID_PROPERTY, Void.class, null); + + Item item = container.addItem(ITEM_ID); + + @SuppressWarnings("unchecked") + Property<int[]> propertyIntArray = item + .getItemProperty(INT_ARRAY_PROPERTY); + propertyIntArray.setValue(new int[] { 1, 1, 2, 3, 5, 8, 13 }); + + Label debugLabel = new Label(INIT_DEBUG_LABEL_CAPTION); + debugLabel.setId(DEBUG_LABEL_ID); + + Grid grid = new Grid(container); + grid.getColumn(INT_ARRAY_PROPERTY).setRenderer(new IntArrayRenderer()); + grid.getColumn(VOID_PROPERTY).setRenderer( + new RowAwareRenderer(debugLabel)); + grid.setSelectionMode(SelectionMode.NONE); + addComponent(grid); + addComponent(debugLabel); + } + + @Override + protected String getTestDescription() { + return "Verifies that renderers operating on other data than " + + "just Strings also work "; + } + + @Override + protected Integer getTicketNumber() { + return Integer.valueOf(13334); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java b/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java new file mode 100644 index 0000000000..571a929c7e --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import com.vaadin.testbench.elements.LabelElement; +import com.vaadin.tests.annotations.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; + +@TestCategory("grid") +public class CustomRendererTest extends MultiBrowserTest { + @Test + public void testIntArrayIsRendered() throws Exception { + openTestURL(); + + GridElement grid = findGrid(); + assertEquals("1 :: 1 :: 2 :: 3 :: 5 :: 8 :: 13", grid.getCell(0, 0) + .getText()); + } + + @Test + public void testRowAwareRenderer() throws Exception { + openTestURL(); + + GridElement grid = findGrid(); + assertEquals("Click me!", grid.getCell(0, 1).getText()); + assertEquals(CustomRenderer.INIT_DEBUG_LABEL_CAPTION, findDebugLabel() + .getText()); + + grid.getCell(0, 1).click(); + assertEquals("row: 0, key: 0", grid.getCell(0, 1).getText()); + assertEquals("key: 0, itemId: " + CustomRenderer.ITEM_ID, + findDebugLabel().getText()); + } + + private GridElement findGrid() { + List<GridElement> elements = $(GridElement.class).all(); + return elements.get(0); + } + + private LabelElement findDebugLabel() { + return $(LabelElement.class).id(CustomRenderer.DEBUG_LABEL_ID); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java new file mode 100644 index 0000000000..fd3c8d5b2f --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java @@ -0,0 +1,260 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elements.LabelElement; +import com.vaadin.testbench.elements.NativeButtonElement; +import com.vaadin.testbench.elements.NativeSelectElement; +import com.vaadin.testbench.elements.ServerClass; +import com.vaadin.tests.annotations.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; +import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers; +import com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers; + +/** + * Tests Grid client side renderers + * + * @since + * @author Vaadin Ltd + */ +@TestCategory("grid") +public class GridClientRenderers extends MultiBrowserTest { + + private static final double SLEEP_MULTIPLIER = 1.2; + private int latency = 0; + + @Override + protected Class<?> getUIClass() { + return GridClientColumnRenderers.class; + } + + @Override + protected String getDeploymentPath() { + String path = super.getDeploymentPath(); + if (latency > 0) { + path += (path.contains("?") ? "&" : "?") + "latency=" + latency; + } + return path; + } + + @ServerClass("com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers.GridController") + public static class MyClientGridElement extends GridElement { + } + + @Override + public void setup() throws Exception { + latency = 0; // reset + super.setup(); + } + + @Test + public void addWidgetRenderer() throws Exception { + openTestURL(); + + // Add widget renderer column + $(NativeSelectElement.class).first().selectByText( + Renderers.WIDGET_RENDERER.toString()); + $(NativeButtonElement.class).caption("Add").first().click(); + + // Click the button in cell 1,1 + TestBenchElement cell = getGrid().getCell(1, 2); + WebElement gwtButton = cell.findElement(By.tagName("button")); + gwtButton.click(); + + // Should be an alert visible + assertEquals("Button did not contain text \"Clicked\"", "Clicked", + gwtButton.getText()); + } + + @Test + public void detachAndAttachGrid() { + openTestURL(); + + // Add widget renderer column + $(NativeSelectElement.class).first().selectByText( + Renderers.WIDGET_RENDERER.toString()); + $(NativeButtonElement.class).caption("Add").first().click(); + + // Detach and re-attach the Grid + $(NativeButtonElement.class).caption("DetachAttach").first().click(); + + // Click the button in cell 1,1 + TestBenchElement cell = getGrid().getCell(1, 2); + WebElement gwtButton = cell.findElement(By.tagName("button")); + gwtButton.click(); + + // Should be an alert visible + assertEquals("Button did not contain text \"Clicked\"", + gwtButton.getText(), "Clicked"); + } + + @Test + public void rowsWithDataHasStyleName() throws Exception { + + // Simulate network latency with 2000ms + latency = 2000; + + openTestURL(); + + sleep((int) (latency * SLEEP_MULTIPLIER)); + + TestBenchElement row = getGrid().getRow(51); + String className = row.getAttribute("class"); + assertFalse( + "Row should not yet contain style name v-grid-row-has-data", + className.contains("v-grid-row-has-data")); + + // Wait for data to arrive + sleep((int) (latency * SLEEP_MULTIPLIER)); + + row = getGrid().getRow(51); + className = row.getAttribute("class"); + assertTrue("Row should now contain style name v-grid-row-has-data", + className.contains("v-grid-row-has-data")); + } + + @Test + public void complexRendererSetVisibleContent() throws Exception { + + DesiredCapabilities desiredCapabilities = getDesiredCapabilities(); + + // Simulate network latency with 2000ms + latency = 2000; + if (BrowserUtil.isIE8(desiredCapabilities)) { + // IE8 is slower than other browsers. Bigger latency is needed for + // stability in this test. + latency = 3000; + } + + // Chrome uses RGB instead of RGBA + String colorRed = "rgba(255, 0, 0, 1)"; + String colorWhite = "rgba(255, 255, 255, 1)"; + if (BrowserUtil.isChrome(desiredCapabilities)) { + colorRed = "rgb(255, 0, 0)"; + colorWhite = "rgb(255, 255, 255)"; + } + + openTestURL(); + + // Test initial renderering with contentVisible = False + TestBenchElement cell = getGrid().getCell(51, 1); + String backgroundColor = cell.getCssValue("backgroundColor"); + assertEquals("Background color was not red.", colorRed, backgroundColor); + + // data arrives... + sleep((int) (latency * SLEEP_MULTIPLIER)); + + // Content becomes visible + cell = getGrid().getCell(51, 1); + backgroundColor = cell.getCssValue("backgroundColor"); + assertNotEquals("Background color was red.", colorRed, backgroundColor); + + // scroll down, new cells becomes contentVisible = False + getGrid().scrollToRow(60); + + // Cell should be red (setContentVisible set cell red) + cell = getGrid().getCell(55, 1); + backgroundColor = cell.getCssValue("backgroundColor"); + assertEquals("Background color was not red.", colorRed, backgroundColor); + + // data arrives... + sleep((int) (latency * SLEEP_MULTIPLIER)); + + // Cell should no longer be red + backgroundColor = cell.getCssValue("backgroundColor"); + assertEquals("Background color was not white", colorWhite, + backgroundColor); + } + + @Test + public void testSortingEvent() throws Exception { + openTestURL(); + + $(NativeButtonElement.class).caption("Trigger sorting event").first() + .click(); + + String consoleText = $(LabelElement.class).id("testDebugConsole") + .getText(); + + assertTrue("Console text as expected", + consoleText.contains("Columns: 1, order: Column 1: ASCENDING")); + + } + + @Test + public void testListSorter() throws Exception { + openTestURL(); + + $(NativeButtonElement.class).caption("Shuffle").first().click(); + + GridElement gridElem = $(MyClientGridElement.class).first(); + + // XXX: DANGER! We'll need to know how many rows the Grid has! + // XXX: Currently, this is impossible; hence the hardcoded value of 70. + + boolean shuffled = false; + for (int i = 1, l = 70; i < l; ++i) { + + String str_a = gridElem.getCell(i - 1, 0).getAttribute("innerHTML"); + String str_b = gridElem.getCell(i, 0).getAttribute("innerHTML"); + + int value_a = Integer.parseInt(str_a); + int value_b = Integer.parseInt(str_b); + + if (value_a > value_b) { + shuffled = true; + break; + } + } + assertTrue("Grid shuffled", shuffled); + + $(NativeButtonElement.class).caption("Test sorting").first().click(); + + for (int i = 1, l = 70; i < l; ++i) { + + String str_a = gridElem.getCell(i - 1, 0).getAttribute("innerHTML"); + String str_b = gridElem.getCell(i, 0).getAttribute("innerHTML"); + + int value_a = Integer.parseInt(str_a); + int value_b = Integer.parseInt(str_b); + + if (value_a > value_b) { + assertTrue("Grid sorted", false); + } + } + } + + private GridElement getGrid() { + return $(MyClientGridElement.class).first(); + } + + private void addColumn(Renderers renderer) { + // Add widget renderer column + $(NativeSelectElement.class).first().selectByText(renderer.toString()); + $(NativeButtonElement.class).caption("Add").first().click(); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColspans.java b/uitest/src/com/vaadin/tests/components/grid/GridColspans.java new file mode 100644 index 0000000000..be12c2bcb2 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridColspans.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridFooter; +import com.vaadin.ui.components.grid.GridFooter.FooterRow; +import com.vaadin.ui.components.grid.GridHeader; +import com.vaadin.ui.components.grid.GridHeader.HeaderRow; +import com.vaadin.ui.components.grid.renderers.NumberRenderer; + +public class GridColspans extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + Indexed dataSource = new IndexedContainer(); + Grid grid; + + dataSource.addContainerProperty("firstName", String.class, ""); + dataSource.addContainerProperty("lastName", String.class, ""); + dataSource.addContainerProperty("streetAddress", String.class, ""); + dataSource.addContainerProperty("zipCode", Integer.class, null); + dataSource.addContainerProperty("city", String.class, ""); + Item i = dataSource.addItem(0); + i.getItemProperty("firstName").setValue("Rudolph"); + i.getItemProperty("lastName").setValue("Reindeer"); + i.getItemProperty("streetAddress").setValue("Ruukinkatu 2-4"); + i.getItemProperty("zipCode").setValue(20540); + i.getItemProperty("city").setValue("Turku"); + grid = new Grid(dataSource); + grid.setWidth("600px"); + grid.getColumn("zipCode").setRenderer(new NumberRenderer()); + addComponent(grid); + + GridHeader header = grid.getHeader(); + HeaderRow row = header.prependRow(); + row.join("firstName", "lastName").setText("Full Name"); + row.join("streetAddress", "zipCode", "city").setText("Address"); + header.prependRow() + .join(dataSource.getContainerPropertyIds().toArray()) + .setText("All the stuff"); + + GridFooter footer = grid.getFooter(); + FooterRow footerRow = footer.appendRow(); + footerRow.join("firstName", "lastName").setText("Full Name"); + footerRow.join("streetAddress", "zipCode", "city").setText("Address"); + footer.appendRow().join(dataSource.getContainerPropertyIds().toArray()) + .setText("All the stuff"); + + footer.setVisible(true); + } + + @Override + protected String getTestDescription() { + return "Grid header and footer colspans"; + } + + @Override + protected Integer getTicketNumber() { + return 13334; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java b/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java new file mode 100644 index 0000000000..dad9399466 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.tests.annotations.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; + +@TestCategory("grid") +public class GridColspansTest extends MultiBrowserTest { + + @Test + public void testHeaderColSpans() { + openTestURL(); + + GridElement grid = $(GridElement.class).first(); + assertEquals("5", grid.getHeaderCell(0, 1).getAttribute("colspan")); + assertEquals("2", grid.getHeaderCell(1, 1).getAttribute("colspan")); + assertEquals("3", grid.getHeaderCell(1, 3).getAttribute("colspan")); + } + + @Test + public void testFooterColSpans() { + openTestURL(); + + GridElement grid = $(GridElement.class).first(); + assertEquals("5", grid.getFooterCell(1, 1).getAttribute("colspan")); + assertEquals("2", grid.getFooterCell(0, 1).getAttribute("colspan")); + assertEquals("3", grid.getFooterCell(0, 3).getAttribute("colspan")); + } + + @Test + public void testActiveHeaderColumnsWithNavigation() throws IOException { + openTestURL(); + + GridElement grid = $(GridElement.class).first(); + grid.getCell(0, 1).click(); + + compareScreen("beforeNavigation"); + + for (int i = 1; i <= 6; ++i) { + assertEquals(true, grid.getFooterCell(1, 1).isActiveHeader()); + assertEquals(i < 3, grid.getFooterCell(0, 1).isActiveHeader()); + assertEquals(i >= 3, grid.getFooterCell(0, 3).isActiveHeader()); + assertEquals(true, grid.getHeaderCell(0, 1).isActiveHeader()); + assertEquals(i < 3, grid.getHeaderCell(1, 1).isActiveHeader()); + assertEquals(i >= 3, grid.getHeaderCell(1, 3).isActiveHeader()); + new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform(); + } + + compareScreen("afterNavigation"); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridElement.java b/uitest/src/com/vaadin/tests/components/grid/GridElement.java new file mode 100644 index 0000000000..bd8cad45c6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridElement.java @@ -0,0 +1,279 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import java.util.ArrayList; +import java.util.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.elements.AbstractComponentElement; +import com.vaadin.testbench.elements.AbstractElement; +import com.vaadin.testbench.elements.ServerClass; + +/** + * TestBench Element API for Grid + * + * @since + * @author Vaadin Ltd + */ +@ServerClass("com.vaadin.ui.components.grid.Grid") +public class GridElement extends AbstractComponentElement { + + public static class GridCellElement extends AbstractElement { + + private String ACTIVE_CLASS_NAME = "-cell-active"; + private String ACTIVE_HEADER_CLASS_NAME = "-header-active"; + + public boolean isActive() { + return getAttribute("class").contains(ACTIVE_CLASS_NAME); + } + + public boolean isActiveHeader() { + return getAttribute("class").contains(ACTIVE_HEADER_CLASS_NAME); + } + } + + public static class GridRowElement extends AbstractElement { + + private String ACTIVE_CLASS_NAME = "-row-active"; + private String SELECTED_CLASS_NAME = "-row-selected"; + + public boolean isActive() { + return getAttribute("class").contains(ACTIVE_CLASS_NAME); + } + + @Override + public boolean isSelected() { + return getAttribute("class").contains(SELECTED_CLASS_NAME); + } + } + + /** + * Scrolls Grid element so that wanted row is displayed + * + * @param index + * Target row + */ + public void scrollToRow(int index) { + try { + getSubPart("#cell[" + index + "]"); + } catch (NoSuchElementException e) { + // Expected, ignore it. + } + } + + /** + * Gets cell element with given row and column index. + * + * @param rowIndex + * Row index + * @param colIndex + * Column index + * @return Cell element with given indices. + */ + public GridCellElement getCell(int rowIndex, int colIndex) { + scrollToRow(rowIndex); + return getSubPart("#cell[" + rowIndex + "][" + colIndex + "]").wrap( + GridCellElement.class); + } + + /** + * Gets row element with given row index. + * + * @param index + * Row index + * @return Row element with given index. + */ + public GridRowElement getRow(int index) { + scrollToRow(index); + return getSubPart("#cell[" + index + "]").wrap(GridRowElement.class); + } + + /** + * Gets header cell element with given row and column index. + * + * @param rowIndex + * Row index + * @param colIndex + * Column index + * @return Header cell element with given indices. + */ + public GridCellElement getHeaderCell(int rowIndex, int colIndex) { + return getSubPart("#header[" + rowIndex + "][" + colIndex + "]").wrap( + GridCellElement.class); + } + + /** + * Gets footer cell element with given row and column index. + * + * @param rowIndex + * Row index + * @param colIndex + * Column index + * @return Footer cell element with given indices. + */ + public GridCellElement getFooterCell(int rowIndex, int colIndex) { + return getSubPart("#footer[" + rowIndex + "][" + colIndex + "]").wrap( + GridCellElement.class); + } + + /** + * Gets list of header cell elements on given row. + * + * @param rowIndex + * Row index + * @return Header cell elements on given row. + */ + public List<GridCellElement> getHeaderCells(int rowIndex) { + List<GridCellElement> headers = new ArrayList<GridCellElement>(); + for (TestBenchElement e : TestBenchElement.wrapElements( + getSubPart("#header[" + rowIndex + "]").findElements( + By.xpath("./th")), getCommandExecutor())) { + headers.add(e.wrap(GridCellElement.class)); + } + return headers; + } + + /** + * Gets list of header cell elements on given row. + * + * @param rowIndex + * Row index + * @return Header cell elements on given row. + */ + public List<GridCellElement> getFooterCells(int rowIndex) { + List<GridCellElement> footers = new ArrayList<GridCellElement>(); + for (TestBenchElement e : TestBenchElement.wrapElements( + getSubPart("#footer[" + rowIndex + "]").findElements( + By.xpath("./td")), getCommandExecutor())) { + footers.add(e.wrap(GridCellElement.class)); + } + return footers; + } + + /** + * Get header row count + * + * @return Header row count + */ + public int getHeaderCount() { + return getSubPart("#header").findElements(By.xpath("./tr")).size(); + } + + /** + * Get footer row count + * + * @return Footer row count + */ + public int getFooterCount() { + return getSubPart("#footer").findElements(By.xpath("./tr")).size(); + } + + /** + * Get a header row by index + * + * @param rowIndex + * Row index + * @return The th element of the row + */ + public WebElement getHeaderRow(int rowIndex) { + return getSubPart("#header[" + rowIndex + "]"); + } + + /** + * Get a footer row by index + * + * @param rowIndex + * Row index + * @return The tr element of the row + */ + public WebElement getFooterRow(int rowIndex) { + return getSubPart("#footer[" + rowIndex + "]"); + } + + /** + * Get the vertical scroll element + * + * @return The element representing the vertical scrollbar + */ + public WebElement getVerticalScroller() { + List<WebElement> rootElements = findElements(By.xpath("./div")); + return rootElements.get(0); + } + + /** + * Get the horizontal scroll element + * + * @return The element representing the horizontal scrollbar + */ + public WebElement getHorizontalScroller() { + List<WebElement> rootElements = findElements(By.xpath("./div")); + return rootElements.get(1); + } + + /** + * Get the header element + * + * @return The thead element + */ + public WebElement getHeader() { + return getSubPart("#header"); + } + + /** + * Get the body element + * + * @return the tbody element + */ + public WebElement getBody() { + return getSubPart("#cell"); + } + + /** + * Get the footer element + * + * @return the tfoot element + */ + public WebElement getFooter() { + return getSubPart("#footer"); + } + + /** + * Get the element wrapping the table element + * + * @return The element that wraps the table element + */ + public WebElement getTableWrapper() { + List<WebElement> rootElements = findElements(By.xpath("./div")); + return rootElements.get(2); + } + + /** + * 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/GridScrolling.java b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java new file mode 100644 index 0000000000..dd86d616b9 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java @@ -0,0 +1,112 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.components.grid.Grid; + +@SuppressWarnings("serial") +public class GridScrolling extends AbstractTestUI { + + private Grid grid; + + private IndexedContainer ds; + + @Override + @SuppressWarnings("unchecked") + protected void setup(VaadinRequest request) { + // Build data source + ds = new IndexedContainer(); + + for (int col = 0; col < 5; col++) { + ds.addContainerProperty("col" + col, String.class, ""); + } + + for (int row = 0; row < 65536; row++) { + Item item = ds.addItem(Integer.valueOf(row)); + for (int col = 0; col < 5; col++) { + item.getItemProperty("col" + col).setValue( + "(" + row + ", " + col + ")"); + } + } + + grid = new Grid(ds); + + HorizontalLayout hl = new HorizontalLayout(); + hl.addComponent(grid); + hl.setMargin(true); + hl.setSpacing(true); + + VerticalLayout vl = new VerticalLayout(); + vl.setSpacing(true); + + // Add scroll buttons + Button scrollUpButton = new Button("Top", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToStart(); + } + }); + scrollUpButton.setSizeFull(); + vl.addComponent(scrollUpButton); + + for (int i = 1; i < 7; ++i) { + final int row = (ds.size() / 7) * i; + Button scrollButton = new Button("Scroll to row " + row, + new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollTo(Integer.valueOf(row), + ScrollDestination.MIDDLE); + } + }); + scrollButton.setSizeFull(); + vl.addComponent(scrollButton); + } + + Button scrollDownButton = new Button("Bottom", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToEnd(); + } + }); + scrollDownButton.setSizeFull(); + vl.addComponent(scrollDownButton); + + hl.addComponent(vl); + addComponent(hl); + } + + @Override + protected String getTestDescription() { + return "Test Grid programmatic scrolling features"; + } + + @Override + protected Integer getTicketNumber() { + return 13327; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java new file mode 100644 index 0000000000..75b83ea3aa --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.Grid.SelectionMode; +import com.vaadin.ui.components.grid.GridColumn; + +public class GridSingleColumn extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + + IndexedContainer indexedContainer = new IndexedContainer(); + indexedContainer.addContainerProperty("column1", String.class, ""); + + for (int i = 0; i < 100; i++) { + Item addItem = indexedContainer.addItem(i); + addItem.getItemProperty("column1").setValue("cell"); + } + + Grid grid = new Grid(indexedContainer); + grid.setSelectionMode(SelectionMode.NONE); + + GridColumn column = grid.getColumn("column1"); + + column.setHeaderCaption("Header"); + + addComponent(grid); + } + + @Override + protected String getTestDescription() { + return "Tests a single column grid"; + } + + @Override + protected Integer getTicketNumber() { + return null; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java new file mode 100644 index 0000000000..2e062f36c6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import org.junit.Ignore; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.tests.annotations.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; + +@TestCategory("grid") +public class GridSingleColumnTest extends MultiBrowserTest { + + /* + * TODO unignore once column header captions are reimplemented + */ + @Test + @Ignore + public void headerIsVisible() { + openTestURL(); + + WebElement header = getDriver().findElement( + By.className("v-grid-header")); + WebElement cell = header.findElement(By.className("v-grid-cell")); + assertThat(cell.getText(), is("Header")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java b/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java new file mode 100644 index 0000000000..142c370e13 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.ui.components.grid.AbstractRenderer; + +public class IntArrayRenderer extends AbstractRenderer<int[]> { + public IntArrayRenderer() { + super(int[].class); + } + + @Override + public Object encode(int[] value) { + try { + return new JSONArray(value); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java b/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java new file mode 100644 index 0000000000..f55f5f064c --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid; + +import org.json.JSONObject; + +import com.vaadin.tests.widgetset.client.grid.RowAwareRendererConnector.RowAwareRendererRpc; +import com.vaadin.ui.Label; +import com.vaadin.ui.components.grid.AbstractRenderer; + +public class RowAwareRenderer extends AbstractRenderer<Void> { + public RowAwareRenderer(final Label debugLabel) { + super(Void.class); + registerRpc(new RowAwareRendererRpc() { + @Override + public void clicky(String key) { + Object itemId = getItemId(key); + debugLabel.setValue("key: " + key + ", itemId: " + itemId); + } + }); + } + + @Override + public Object encode(Void value) { + return JSONObject.NULL; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java new file mode 100644 index 0000000000..4c5e703b82 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.UI; + +/** + * Initializer shell for GridClientBasicFeatures test application + * + * @since + * @author Vaadin Ltd + */ +@Widgetset(TestingWidgetSet.NAME) +public class GridBasicClientFeatures extends UI { + + public class GridTestComponent extends AbstractComponent { + } + + @Override + protected void init(VaadinRequest request) { + setContent(new GridTestComponent()); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java new file mode 100644 index 0000000000..e3318fe650 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +/** + * Variant of GridBasicFeaturesTest to be used with GridBasicClientFeatures. + * + * @since + * @author Vaadin Ltd + */ +public abstract class GridBasicClientFeaturesTest extends GridBasicFeaturesTest { + + @Override + protected Class<?> getUIClass() { + return GridBasicClientFeatures.class; + } + + @Override + protected void selectMenu(String menuCaption) { + WebElement menuElement = getMenuElement(menuCaption); + Dimension size = menuElement.getSize(); + new Actions(getDriver()).moveToElement(menuElement, size.width - 10, + size.height / 2).perform(); + } + + private WebElement getMenuElement(String menuCaption) { + return getDriver().findElement( + By.xpath("//td[text() = '" + menuCaption + "']")); + } + + @Override + protected void selectMenuPath(String... menuCaptions) { + new Actions(getDriver()).moveToElement(getMenuElement(menuCaptions[0])) + .click().perform(); + for (int i = 1; i < menuCaptions.length - 1; ++i) { + selectMenu(menuCaptions[i]); + new Actions(getDriver()).moveByOffset(20, 0).perform(); + } + new Actions(getDriver()) + .moveToElement( + getMenuElement(menuCaptions[menuCaptions.length - 1])) + .click().perform(); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java new file mode 100644 index 0000000000..d54b1838ea --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java @@ -0,0 +1,701 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.grid.SortDirection; +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.components.grid.Grid; +import com.vaadin.ui.components.grid.Grid.SelectionMode; +import com.vaadin.ui.components.grid.GridColumn; +import com.vaadin.ui.components.grid.GridFooter; +import com.vaadin.ui.components.grid.GridFooter.FooterCell; +import com.vaadin.ui.components.grid.GridHeader; +import com.vaadin.ui.components.grid.GridHeader.HeaderCell; +import com.vaadin.ui.components.grid.GridHeader.HeaderRow; +import com.vaadin.ui.components.grid.SortOrderChangeEvent; +import com.vaadin.ui.components.grid.SortOrderChangeListener; +import com.vaadin.ui.components.grid.renderers.DateRenderer; +import com.vaadin.ui.components.grid.renderers.HtmlRenderer; +import com.vaadin.ui.components.grid.renderers.NumberRenderer; +import com.vaadin.ui.components.grid.sort.Sort; +import com.vaadin.ui.components.grid.sort.SortOrder; + +/** + * Tests the basic features like columns, footers and headers + * + * @since + * @author Vaadin Ltd + */ +public class GridBasicFeatures extends AbstractComponentTest<Grid> { + + private static final int MANUALLY_FORMATTED_COLUMNS = 5; + public static final int COLUMNS = 12; + public static final int ROWS = 1000; + + private int columnGroupRows = 0; + private IndexedContainer ds; + + @Override + @SuppressWarnings("unchecked") + protected Grid constructComponent() { + + // Build data source + ds = new IndexedContainer() { + @Override + public List<Object> getItemIds(int startIndex, int numberOfIds) { + log("Requested items " + startIndex + " - " + + (startIndex + numberOfIds)); + return super.getItemIds(startIndex, numberOfIds); + } + }; + + { + int col = 0; + for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; col++) { + ds.addContainerProperty(getColumnProperty(col), String.class, + ""); + } + + ds.addContainerProperty(getColumnProperty(col++), Integer.class, + Integer.valueOf(0)); + ds.addContainerProperty(getColumnProperty(col++), Date.class, + new Date()); + ds.addContainerProperty(getColumnProperty(col++), String.class, ""); + + // Random numbers + ds.addContainerProperty(getColumnProperty(col++), Integer.class, 0); + ds.addContainerProperty(getColumnProperty(col++), Integer.class, 0); + + } + + { + Random rand = new Random(); + rand.setSeed(13334); + long timestamp = 0; + for (int row = 0; row < ROWS; row++) { + Item item = ds.addItem(Integer.valueOf(row)); + int col = 0; + for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; col++) { + item.getItemProperty(getColumnProperty(col)).setValue( + "(" + row + ", " + col + ")"); + } + item.getItemProperty(getColumnProperty(col++)).setValue( + Integer.valueOf(row)); + item.getItemProperty(getColumnProperty(col++)).setValue( + new Date(timestamp)); + timestamp += 91250000; // a bit over a day, just to get + // variation + item.getItemProperty(getColumnProperty(col++)).setValue( + "<b>" + row + "</b>"); + + // Random numbers + item.getItemProperty(getColumnProperty(col++)).setValue( + rand.nextInt()); + // Random between 0 - 5 to test multisorting + item.getItemProperty(getColumnProperty(col++)).setValue( + rand.nextInt(5)); + } + } + + // Create grid + Grid grid = new Grid(ds); + + { + int col = grid.getContainerDatasource().getContainerPropertyIds() + .size() + - MANUALLY_FORMATTED_COLUMNS; + grid.getColumn(getColumnProperty(col++)).setRenderer( + new NumberRenderer(new DecimalFormat("0,000.00", + DecimalFormatSymbols.getInstance(new Locale("fi", + "FI"))))); + grid.getColumn(getColumnProperty(col++)).setRenderer( + new DateRenderer(new SimpleDateFormat("dd.MM.yy HH:mm"))); + grid.getColumn(getColumnProperty(col++)).setRenderer( + new HtmlRenderer()); + grid.getColumn(getColumnProperty(col++)).setRenderer( + new NumberRenderer()); + grid.getColumn(getColumnProperty(col++)).setRenderer( + new NumberRenderer()); + } + + // Create footer + GridFooter footer = grid.getFooter(); + footer.appendRow(); + footer.setVisible(false); + + // Add footer values (header values are automatically created) + for (int col = 0; col < COLUMNS; col++) { + footer.getRow(0).getCell(getColumnProperty(col)) + .setText("Footer " + col); + } + + // Set varying column widths + for (int col = 0; col < COLUMNS; col++) { + grid.getColumn(getColumnProperty(col)).setWidth(100 + col * 50); + } + + grid.addSortOrderChangeListener(new SortOrderChangeListener() { + @Override + public void sortOrderChange(SortOrderChangeEvent event) { + + String origin; + switch (event.getOriginator()) { + case API: + origin = "API"; + break; + case INTERNAL: + origin = "INTERNAL"; + break; + case USER: + origin = "USER"; + break; + default: + origin = "!!! ERROR !!!"; + break; + } + + log("Sort order: " + event.getSortOrder() + " by " + origin); + } + }); + + grid.setSelectionMode(SelectionMode.NONE); + + createGridActions(); + + createColumnActions(); + + createHeaderActions(); + + createFooterActions(); + + createRowActions(); + + addHeightActions(); + + return grid; + } + + protected void createGridActions() { + LinkedHashMap<String, String> primaryStyleNames = new LinkedHashMap<String, String>(); + primaryStyleNames.put("v-grid", "v-grid"); + primaryStyleNames.put("v-escalator", "v-escalator"); + primaryStyleNames.put("my-grid", "my-grid"); + + createMultiClickAction("Primary style name", "State", + primaryStyleNames, new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.setPrimaryStyleName(value); + + } + }, primaryStyleNames.get("v-grid")); + + LinkedHashMap<String, SelectionMode> selectionModes = new LinkedHashMap<String, Grid.SelectionMode>(); + selectionModes.put("single", SelectionMode.SINGLE); + selectionModes.put("multi", SelectionMode.MULTI); + selectionModes.put("none", SelectionMode.NONE); + createSelectAction("Selection mode", "State", selectionModes, "none", + new Command<Grid, Grid.SelectionMode>() { + @Override + public void execute(Grid grid, SelectionMode selectionMode, + Object data) { + grid.setSelectionMode(selectionMode); + } + }); + + LinkedHashMap<String, List<SortOrder>> sortableProperties = new LinkedHashMap<String, List<SortOrder>>(); + for (Object propertyId : ds.getSortableContainerPropertyIds()) { + sortableProperties.put(propertyId + ", ASC", Sort.by(propertyId) + .build()); + sortableProperties.put(propertyId + ", DESC", + Sort.by(propertyId, SortDirection.DESCENDING).build()); + } + createSelectAction("Sort by column", "State", sortableProperties, + "Column 9, ascending", new Command<Grid, List<SortOrder>>() { + @Override + public void execute(Grid grid, List<SortOrder> sortOrder, + Object data) { + grid.setSortOrder(sortOrder); + } + }); + } + + protected void createHeaderActions() { + createCategory("Header", null); + + createBooleanAction("Visible", "Header", true, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, Object data) { + grid.getHeader().setVisible(value); + } + }); + + LinkedHashMap<String, String> defaultRows = new LinkedHashMap<String, String>(); + defaultRows.put("Top", "Top"); + defaultRows.put("Bottom", "Bottom"); + defaultRows.put("Unset", "Unset"); + + createMultiClickAction("Default row", "Header", defaultRows, + new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + HeaderRow defaultRow = null; + GridHeader header = grid.getHeader(); + if (value.equals("Top")) { + defaultRow = header.getRow(0); + } else if (value.equals("Bottom")) { + defaultRow = header.getRow(header.getRowCount() - 1); + } + header.setDefaultRow(defaultRow); + } + + }, defaultRows.get("Top")); + + createClickAction("Prepend row", "Header", new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getHeader().prependRow(); + } + + }, null); + createClickAction("Append row", "Header", new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getHeader().appendRow(); + } + + }, null); + + createClickAction("Remove top row", "Header", + new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getHeader().removeRow(0); + } + + }, null); + createClickAction("Remove bottom row", "Header", + new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getHeader().removeRow( + grid.getHeader().getRowCount() - 1); + } + + }, null); + } + + protected void createFooterActions() { + createCategory("Footer", null); + + createBooleanAction("Visible", "Footer", false, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, Object data) { + grid.getFooter().setVisible(value); + } + }); + + createClickAction("Prepend row", "Footer", new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getFooter().prependRow(); + } + + }, null); + createClickAction("Append row", "Footer", new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getFooter().appendRow(); + } + + }, null); + + createClickAction("Remove top row", "Footer", + new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getFooter().removeRow(0); + } + + }, null); + createClickAction("Remove bottom row", "Footer", + new Command<Grid, Object>() { + + @Override + public void execute(Grid grid, Object value, Object data) { + grid.getFooter().removeRow( + grid.getFooter().getRowCount() - 1); + } + + }, null); + } + + protected void createColumnActions() { + createCategory("Columns", null); + + for (int c = 0; c < COLUMNS; c++) { + createCategory(getColumnProperty(c), "Columns"); + + createBooleanAction("Visible", getColumnProperty(c), true, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setVisible(!column.isVisible()); + } + }, c); + + createClickAction("Remove", getColumnProperty(c), + new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.getContainerDatasource() + .removeContainerProperty( + getColumnProperty((Integer) data)); + } + }, null, c); + + createClickAction("Freeze", getColumnProperty(c), + new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.setLastFrozenPropertyId(getColumnProperty((Integer) data)); + } + }, null, c); + + createBooleanAction("Sortable", getColumnProperty(c), true, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setSortable(value); + } + }, c); + + createCategory("Column " + c + " Width", getColumnProperty(c)); + + createClickAction("Auto", "Column " + c + " Width", + new Command<Grid, Integer>() { + + @Override + public void execute(Grid grid, Integer value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setWidthUndefined(); + } + }, -1, c); + + for (int w = 50; w < 300; w += 50) { + createClickAction(w + "px", "Column " + c + " Width", + new Command<Grid, Integer>() { + + @Override + public void execute(Grid grid, Integer value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setWidth(value); + } + }, w, c); + } + + LinkedHashMap<String, GridStaticCellType> defaultRows = new LinkedHashMap<String, GridStaticCellType>(); + defaultRows.put("Text Header", GridStaticCellType.TEXT); + defaultRows.put("Html Header ", GridStaticCellType.HTML); + defaultRows.put("Widget Header", GridStaticCellType.WIDGET); + + createMultiClickAction("Header Type", getColumnProperty(c), + defaultRows, new Command<Grid, GridStaticCellType>() { + + @Override + public void execute(Grid grid, + GridStaticCellType value, Object columnIndex) { + final Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + final HeaderCell cell = grid.getHeader() + .getDefaultRow().getCell(propertyId); + switch (value) { + case TEXT: + cell.setText("Text Header"); + break; + case HTML: + cell.setHtml("HTML Header"); + break; + case WIDGET: + cell.setComponent(new Button("Button Header", + new ClickListener() { + + @Override + public void buttonClick( + ClickEvent event) { + log("Button clicked!"); + } + })); + default: + break; + } + } + + }, c); + + defaultRows = new LinkedHashMap<String, GridStaticCellType>(); + defaultRows.put("Text Footer", GridStaticCellType.TEXT); + defaultRows.put("Html Footer", GridStaticCellType.HTML); + defaultRows.put("Widget Footer", GridStaticCellType.WIDGET); + + createMultiClickAction("Footer Type", getColumnProperty(c), + defaultRows, new Command<Grid, GridStaticCellType>() { + + @Override + public void execute(Grid grid, + GridStaticCellType value, Object columnIndex) { + final Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + final FooterCell cell = grid.getFooter().getRow(0) + .getCell(propertyId); + switch (value) { + case TEXT: + cell.setText("Text Footer"); + break; + case HTML: + cell.setHtml("HTML Footer"); + break; + case WIDGET: + cell.setComponent(new Button("Button Footer", + new ClickListener() { + + @Override + public void buttonClick( + ClickEvent event) { + log("Button clicked!"); + } + })); + default: + break; + } + } + + }, c); + } + } + + private static String getColumnProperty(int c) { + return "Column " + c; + } + + protected void createRowActions() { + createCategory("Body rows", null); + + createClickAction("Add first row", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Item item = ds.addItemAt(0, new Object()); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("newcell: " + i); + } + } + }, null); + + createClickAction("Remove first row", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + ds.removeItem(firstItemId); + } + }, null); + + createClickAction("Modify first row (getItemProperty)", "Body rows", + new Command<Grid, String>() { + @SuppressWarnings("unchecked") + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + Item item = ds.getItem(firstItemId); + for (int i = 0; i < COLUMNS; i++) { + Property<?> property = item + .getItemProperty(getColumnProperty(i)); + if (property.getType().equals(String.class)) { + ((Property<String>) property) + .setValue("modified: " + i); + } + } + } + }, null); + + createClickAction("Modify first row (getContainerProperty)", + "Body rows", new Command<Grid, String>() { + @SuppressWarnings("unchecked") + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + for (Object containerPropertyId : ds + .getContainerPropertyIds()) { + Property<?> property = ds.getContainerProperty( + firstItemId, containerPropertyId); + if (property.getType().equals(String.class)) { + ((Property<String>) property) + .setValue("modified: " + + containerPropertyId); + } + } + } + }, null); + + createBooleanAction("Select first row", "Body rows", false, + new Command<Grid, Boolean>() { + @Override + public void execute(Grid grid, Boolean select, Object data) { + final Object firstItemId = grid + .getContainerDatasource().firstItemId(); + if (select.booleanValue()) { + grid.select(firstItemId); + } else { + grid.deselect(firstItemId); + } + } + }); + + createClickAction("Remove all rows", "Body rows", + new Command<Grid, String>() { + @SuppressWarnings("unchecked") + @Override + public void execute(Grid c, String value, Object data) { + ds.removeAllItems(); + } + }, null); + } + + @SuppressWarnings("boxing") + protected void addHeightActions() { + createCategory("Height by Rows", "Size"); + + createBooleanAction("HeightMode Row", "Size", false, + new Command<Grid, Boolean>() { + @Override + public void execute(Grid c, Boolean heightModeByRows, + Object data) { + c.setHeightMode(heightModeByRows ? HeightMode.ROW + : HeightMode.CSS); + } + }, null); + + addActionForHeightByRows(1d / 3d); + addActionForHeightByRows(2d / 3d); + + for (double i = 1; i < 5; i++) { + addActionForHeightByRows(i); + addActionForHeightByRows(i + 1d / 3d); + addActionForHeightByRows(i + 2d / 3d); + } + + Command<Grid, String> sizeCommand = new Command<Grid, String>() { + @Override + public void execute(Grid grid, String height, Object data) { + grid.setHeight(height); + } + }; + + createCategory("Height", "Size"); + // header 20px + scrollbar 16px = 36px baseline + createClickAction("86px (no drag scroll select)", "Height", + sizeCommand, "86px"); + createClickAction("96px (drag scroll select limit)", "Height", + sizeCommand, "96px"); + createClickAction("106px (drag scroll select enabled)", "Height", + sizeCommand, "106px"); + } + + private void addActionForHeightByRows(final Double i) { + DecimalFormat df = new DecimalFormat("0.00"); + createClickAction(df.format(i) + " rows", "Height by Rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + c.setHeightByRows(i); + } + }, null); + } + + @Override + protected Integer getTicketNumber() { + return 12829; + } + + @Override + protected Class<Grid> getTestClass() { + return Grid.class; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java new file mode 100644 index 0000000000..6ef0ab5006 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import java.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.annotations.TestCategory; +import com.vaadin.tests.components.grid.GridElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +@TestCategory("grid") +public abstract class GridBasicFeaturesTest extends MultiBrowserTest { + + @Override + protected DesiredCapabilities getDesiredCapabilities() { + DesiredCapabilities dCap = super.getDesiredCapabilities(); + if (BrowserUtil.isIE(dCap)) { + dCap.setCapability("requireWindowFocus", true); + } + return super.getDesiredCapabilities(); + } + + @Override + protected Class<?> getUIClass() { + return GridBasicFeatures.class; + } + + protected void selectSubMenu(String menuCaption) { + selectMenu(menuCaption); + new Actions(getDriver()).moveByOffset(100, 0).build().perform(); + } + + protected void selectMenu(String menuCaption) { + getDriver().findElement( + By.xpath("//span[text() = '" + menuCaption + "']")).click(); + } + + protected void selectMenuPath(String... menuCaptions) { + selectMenu(menuCaptions[0]); + for (int i = 1; i < menuCaptions.length; i++) { + selectSubMenu(menuCaptions[i]); + } + } + + protected GridElement getGridElement() { + return ((TestBenchElement) findElement(By.id("testComponent"))) + .wrap(GridElement.class); + } + + protected void scrollGridVerticallyTo(double px) { + executeScript("arguments[0].scrollTop = " + px, + getGridVerticalScrollbar()); + } + + protected List<TestBenchElement> getGridHeaderRowCells() { + List<TestBenchElement> headerCells = new ArrayList<TestBenchElement>(); + for (int i = 0; i < getGridElement().getHeaderCount(); ++i) { + headerCells.addAll(getGridElement().getHeaderCells(i)); + } + return headerCells; + } + + protected List<TestBenchElement> getGridFooterRowCells() { + List<TestBenchElement> footerCells = new ArrayList<TestBenchElement>(); + for (int i = 0; i < getGridElement().getFooterCount(); ++i) { + footerCells.addAll(getGridElement().getFooterCells(i)); + } + return footerCells; + } + + private Object executeScript(String script, WebElement element) { + final WebDriver driver = getDriver(); + if (driver instanceof JavascriptExecutor) { + final JavascriptExecutor je = (JavascriptExecutor) driver; + return je.executeScript(script, element); + } else { + throw new IllegalStateException("current driver " + + getDriver().getClass().getName() + " is not a " + + JavascriptExecutor.class.getSimpleName()); + } + } + + private WebElement getGridVerticalScrollbar() { + return getDriver() + .findElement( + By.xpath("//div[contains(@class, \"v-grid-scroller-vertical\")]")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java new file mode 100644 index 0000000000..ece9fdf7d7 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest; +import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget; + +public class GridClientColumnPropertiesTest extends GridBasicClientFeaturesTest { + + @Test + public void initialColumnWidths() { + openTestURL(); + + for (int col = 0; col < GridBasicClientFeaturesWidget.COLUMNS; col++) { + int width = getGridElement().getCell(0, col).getSize().getWidth(); + if (col <= 6) { + // Growing column widths + assertEquals(50 + col * 25, width); + } else { + assertEquals(100, width); + } + } + } + + @Test + public void testChangingColumnWidth() { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 0", "Width", "50px"); + int width = getGridElement().getCell(0, 0).getSize().getWidth(); + assertEquals(50, width); + + selectMenuPath("Component", "Columns", "Column 0", "Width", "200px"); + width = getGridElement().getCell(0, 0).getSize().getWidth(); + assertEquals(200, width); + + selectMenuPath("Component", "Columns", "Column 0", "Width", "auto"); + width = getGridElement().getCell(0, 0).getSize().getWidth(); + assertEquals(100, width); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java new file mode 100644 index 0000000000..fe81380296 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.By; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest; + +public class GridClientKeyEventsTest extends GridBasicClientFeaturesTest { + + private List<String> eventOrder = Arrays.asList("keydown", "keyup", + "keypress"); + + @Test + public void testBodyKeyEvents() throws IOException { + openTestURL(); + + getGridElement().getCell(2, 2).click(); + + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + for (int i = 0; i < 3; ++i) { + assertEquals("Body key event handler was not called.", "(2, 2) " + + eventOrder.get(i) + " 13", + findElements(By.className("v-label")).get(i * 3).getText()); + + assertTrue("Header key event handler got called unexpectedly.", + findElements(By.className("v-label")).get(i * 3 + 1) + .getText().isEmpty()); + assertTrue("Footer key event handler got called unexpectedly.", + findElements(By.className("v-label")).get(i * 3 + 2) + .getText().isEmpty()); + } + + } + + @Test + public void testHeaderKeyEvents() throws IOException { + openTestURL(); + + getGridElement().getHeaderCell(0, 2).click(); + + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + for (int i = 0; i < 3; ++i) { + assertEquals("Header key event handler was not called.", "(0, 2) " + + eventOrder.get(i) + " 13", + findElements(By.className("v-label")).get(i * 3 + 1) + .getText()); + + assertTrue("Body key event handler got called unexpectedly.", + findElements(By.className("v-label")).get(i * 3).getText() + .isEmpty()); + assertTrue("Footer key event handler got called unexpectedly.", + findElements(By.className("v-label")).get(i * 3 + 2) + .getText().isEmpty()); + } + } + + @Test + public void testFooterKeyEvents() throws IOException { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + getGridElement().getFooterCell(0, 2).click(); + + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + for (int i = 0; i < 3; ++i) { + assertEquals("Footer key event handler was not called.", "(0, 2) " + + eventOrder.get(i) + " 13", + findElements(By.className("v-label")).get(i * 3 + 2) + .getText()); + + assertTrue("Body key event handler got called unexpectedly.", + findElements(By.className("v-label")).get(i * 3).getText() + .isEmpty()); + assertTrue("Header key event handler got called unexpectedly.", + findElements(By.className("v-label")).get(i * 3 + 1) + .getText().isEmpty()); + + } + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java new file mode 100644 index 0000000000..4b47837887 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest; + +public class GridClientSelectionTest extends GridBasicClientFeaturesTest { + + @Test + public void testChangeSelectionMode() { + openTestURL(); + + selectMenuPath("Component", "State", "Selection mode", "none"); + assertTrue("First column was selection column", getGridElement() + .getCell(0, 0).getText().equals("(0, 0)")); + selectMenuPath("Component", "State", "Selection mode", "multi"); + assertTrue("First column was not selection column", getGridElement() + .getCell(0, 1).getText().equals("(0, 0)")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java new file mode 100644 index 0000000000..8124e5361f --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.tests.components.grid.GridElement.GridCellElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures; + +public class GridFooterTest extends GridStaticSectionTest { + + @Test + public void testDefaultFooter() { + openTestURL(); + + // Footer should have zero rows by default + assertFooterCount(0); + } + + @Test + public void testFooterVisibility() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Visible"); + + assertFooterCount(0); + + selectMenuPath("Component", "Footer", "Append row"); + + assertFooterCount(0); + + selectMenuPath("Component", "Footer", "Visible"); + + assertFooterCount(1); + } + + @Test + public void testAddRows() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + assertFooterCount(1); + assertFooterTexts(0, 0); + + selectMenuPath("Component", "Footer", "Prepend row"); + + assertFooterCount(2); + assertFooterTexts(1, 0); + assertFooterTexts(0, 1); + + selectMenuPath("Component", "Footer", "Append row"); + + assertFooterCount(3); + assertFooterTexts(1, 0); + assertFooterTexts(0, 1); + assertFooterTexts(2, 2); + } + + @Test + public void testRemoveRows() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Prepend row"); + selectMenuPath("Component", "Footer", "Append row"); + + selectMenuPath("Component", "Footer", "Remove top row"); + + assertFooterCount(1); + assertFooterTexts(1, 0); + + selectMenuPath("Component", "Footer", "Remove bottom row"); + assertFooterCount(0); + } + + @Test + public void joinColumnsByCells() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + selectMenuPath("Component", "Footer", "Row 1", "Join column cells 0, 1"); + + GridCellElement spannedCell = getGridElement().getFooterCell(0, 0); + assertTrue(spannedCell.isDisplayed()); + assertEquals("2", spannedCell.getAttribute("colspan")); + + GridCellElement hiddenCell = getGridElement().getFooterCell(0, 1); + assertFalse(hiddenCell.isDisplayed()); + } + + @Test + public void joinColumnsByColumns() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2"); + + GridCellElement spannedCell = getGridElement().getFooterCell(0, 1); + assertTrue(spannedCell.isDisplayed()); + assertEquals("2", spannedCell.getAttribute("colspan")); + + GridCellElement hiddenCell = getGridElement().getFooterCell(0, 2); + assertFalse(hiddenCell.isDisplayed()); + } + + @Test + public void joinAllColumnsInRow() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + selectMenuPath("Component", "Footer", "Row 1", "Join all columns"); + + GridCellElement spannedCell = getGridElement().getFooterCell(0, 0); + assertTrue(spannedCell.isDisplayed()); + assertEquals("" + GridBasicFeatures.COLUMNS, + spannedCell.getAttribute("colspan")); + + for (int columnIndex = 1; columnIndex < GridBasicFeatures.COLUMNS; columnIndex++) { + GridCellElement hiddenCell = getGridElement().getFooterCell(0, + columnIndex); + assertFalse(hiddenCell.isDisplayed()); + } + } + + @Test + public void testInitialCellTypes() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + GridCellElement textCell = getGridElement().getFooterCell(0, 0); + assertEquals("Footer (0,0)", textCell.getText()); + + GridCellElement widgetCell = getGridElement().getFooterCell(0, 1); + assertTrue(widgetCell.isElementPresent(By.className("gwt-HTML"))); + + GridCellElement htmlCell = getGridElement().getFooterCell(0, 2); + assertHTML("<b>Footer (0,2)</b>", htmlCell); + } + + @Test + public void testDynamicallyChangingCellType() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + selectMenuPath("Component", "Columns", "Column 0", "Footer Type", + "Widget Footer"); + GridCellElement widgetCell = getGridElement().getFooterCell(0, 0); + assertTrue(widgetCell.isElementPresent(By.className("gwt-Button"))); + + selectMenuPath("Component", "Columns", "Column 1", "Footer Type", + "HTML Footer"); + GridCellElement htmlCell = getGridElement().getFooterCell(0, 1); + assertHTML("<b>HTML Footer</b>", htmlCell); + + selectMenuPath("Component", "Columns", "Column 2", "Footer Type", + "Text Footer"); + GridCellElement textCell = getGridElement().getFooterCell(0, 2); + assertEquals("Text Footer", textCell.getText()); + } + + @Test + public void testCellWidgetInteraction() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Footer", "Append row"); + + selectMenuPath("Component", "Columns", "Column 0", "Footer Type", + "Widget Footer"); + GridCellElement widgetCell = getGridElement().getFooterCell(0, 0); + WebElement button = widgetCell.findElement(By.className("gwt-Button")); + + assertNotEquals("Clicked", button.getText()); + + button.click(); + + assertEquals("Clicked", button.getText()); + } + + private void assertFooterCount(int count) { + assertEquals("footer count", count, getGridElement().getFooterCount()); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java new file mode 100644 index 0000000000..c528571a2e --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java @@ -0,0 +1,359 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.components.grid.GridElement.GridCellElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures; + +public class GridHeaderTest extends GridStaticSectionTest { + + @Test + public void testDefaultHeader() throws Exception { + openTestURL(); + + assertHeaderCount(1); + assertHeaderTexts(0, 0); + } + + @Test + public void testHeaderVisibility() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Visible"); + + assertHeaderCount(0); + + selectMenuPath("Component", "Header", "Append row"); + + assertHeaderCount(0); + + selectMenuPath("Component", "Header", "Visible"); + + assertHeaderCount(2); + } + + @Test + public void testHeaderCaptions() throws Exception { + openTestURL(); + + assertHeaderTexts(0, 0); + } + + @Test + public void testHeadersWithInvisibleColumns() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 1", "Visible"); + selectMenuPath("Component", "Columns", "Column 3", "Visible"); + + List<TestBenchElement> cells = getGridHeaderRowCells(); + assertEquals(GridBasicFeatures.COLUMNS - 2, cells.size()); + + assertText("Header (0,0)", cells.get(0)); + assertHTML("<b>Header (0,2)</b>", cells.get(1)); + assertHTML("<b>Header (0,4)</b>", cells.get(2)); + + selectMenuPath("Component", "Columns", "Column 3", "Visible"); + + cells = getGridHeaderRowCells(); + assertEquals(GridBasicFeatures.COLUMNS - 1, cells.size()); + + assertText("Header (0,0)", cells.get(0)); + assertHTML("<b>Header (0,2)</b>", cells.get(1)); + assertText("Header (0,3)", cells.get(2)); + assertHTML("<b>Header (0,4)</b>", cells.get(3)); + } + + @Test + public void testAddRows() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Append row"); + + assertHeaderCount(2); + assertHeaderTexts(0, 0); + assertHeaderTexts(1, 1); + + selectMenuPath("Component", "Header", "Prepend row"); + + assertHeaderCount(3); + assertHeaderTexts(2, 0); + assertHeaderTexts(0, 1); + assertHeaderTexts(1, 2); + + selectMenuPath("Component", "Header", "Append row"); + + assertHeaderCount(4); + assertHeaderTexts(2, 0); + assertHeaderTexts(0, 1); + assertHeaderTexts(1, 2); + assertHeaderTexts(3, 3); + } + + @Test + public void testRemoveRows() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Prepend row"); + selectMenuPath("Component", "Header", "Append row"); + + selectMenuPath("Component", "Header", "Remove top row"); + + assertHeaderCount(2); + assertHeaderTexts(0, 0); + assertHeaderTexts(2, 1); + + selectMenuPath("Component", "Header", "Remove bottom row"); + assertHeaderCount(1); + assertHeaderTexts(0, 0); + } + + @Test + public void testDefaultRow() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 0", "Sortable"); + + GridCellElement headerCell = getGridElement().getHeaderCell(0, 0); + + headerCell.click(); + + assertTrue(hasClassName(headerCell, "sort-asc")); + + headerCell.click(); + + assertFalse(hasClassName(headerCell, "sort-asc")); + assertTrue(hasClassName(headerCell, "sort-desc")); + + selectMenuPath("Component", "Header", "Prepend row"); + selectMenuPath("Component", "Header", "Default row", "Top"); + + assertFalse(hasClassName(headerCell, "sort-desc")); + headerCell = getGridElement().getHeaderCell(0, 0); + assertTrue(hasClassName(headerCell, "sort-desc")); + + selectMenuPath("Component", "Header", "Default row", "Unset"); + + assertFalse(hasClassName(headerCell, "sort-desc")); + } + + @Test + public void joinHeaderColumnsByCells() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Append row"); + + selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1"); + + GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0); + assertTrue(spannedCell.isDisplayed()); + assertEquals("2", spannedCell.getAttribute("colspan")); + + GridCellElement hiddenCell = getGridElement().getHeaderCell(1, 1); + assertFalse(hiddenCell.isDisplayed()); + } + + @Test + public void joinHeaderColumnsByColumns() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Append row"); + + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + + GridCellElement spannedCell = getGridElement().getHeaderCell(1, 1); + assertTrue(spannedCell.isDisplayed()); + assertEquals("2", spannedCell.getAttribute("colspan")); + + GridCellElement hiddenCell = getGridElement().getHeaderCell(1, 2); + assertFalse(hiddenCell.isDisplayed()); + } + + @Test + public void joinAllColumnsInHeaderRow() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Append row"); + + selectMenuPath("Component", "Header", "Row 2", "Join all columns"); + + GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0); + assertTrue(spannedCell.isDisplayed()); + assertEquals("" + GridBasicFeatures.COLUMNS, + spannedCell.getAttribute("colspan")); + + for (int columnIndex = 1; columnIndex < GridBasicFeatures.COLUMNS; columnIndex++) { + GridCellElement hiddenCell = getGridElement().getHeaderCell(1, + columnIndex); + assertFalse(hiddenCell.isDisplayed()); + } + } + + @Test + public void hideFirstColumnInColspan() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Append row"); + + selectMenuPath("Component", "Header", "Row 2", "Join all columns"); + + int visibleColumns = GridBasicFeatures.COLUMNS; + + GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0); + assertTrue(spannedCell.isDisplayed()); + assertEquals("" + visibleColumns, spannedCell.getAttribute("colspan")); + + selectMenuPath("Component", "Columns", "Column 0", "Visible"); + visibleColumns--; + + spannedCell = getGridElement().getHeaderCell(1, 0); + assertTrue(spannedCell.isDisplayed()); + assertEquals("" + visibleColumns, spannedCell.getAttribute("colspan")); + } + + @Test + public void multipleColspanAndMultipleHiddenColumns() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Header", "Append row"); + + // Join columns [1,2] and [3,4,5] + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + GridCellElement spannedCell = getGridElement().getHeaderCell(1, 1); + assertEquals("2", spannedCell.getAttribute("colspan")); + + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + spannedCell = getGridElement().getHeaderCell(1, 3); + assertEquals("3", spannedCell.getAttribute("colspan")); + + selectMenuPath("Component", "Columns", "Column 2", "Visible"); + spannedCell = getGridElement().getHeaderCell(1, 1); + assertEquals("1", spannedCell.getAttribute("colspan")); + + // Ensure the second colspan is preserved (shifts one index to the left) + spannedCell = getGridElement().getHeaderCell(1, 2); + assertEquals("3", spannedCell.getAttribute("colspan")); + + selectMenuPath("Component", "Columns", "Column 4", "Visible"); + + // First reduced colspan is reduced + spannedCell = getGridElement().getHeaderCell(1, 1); + assertEquals("1", spannedCell.getAttribute("colspan")); + + // Second colspan is also now reduced + spannedCell = getGridElement().getHeaderCell(1, 2); + assertEquals("2", spannedCell.getAttribute("colspan")); + + // Show columns again + selectMenuPath("Component", "Columns", "Column 2", "Visible"); + selectMenuPath("Component", "Columns", "Column 4", "Visible"); + + spannedCell = getGridElement().getHeaderCell(1, 1); + assertEquals("2", spannedCell.getAttribute("colspan")); + spannedCell = getGridElement().getHeaderCell(1, 3); + assertEquals("3", spannedCell.getAttribute("colspan")); + + } + + @Test + public void testInitialCellTypes() throws Exception { + openTestURL(); + + GridCellElement textCell = getGridElement().getHeaderCell(0, 0); + assertEquals("Header (0,0)", textCell.getText()); + + GridCellElement widgetCell = getGridElement().getHeaderCell(0, 1); + assertTrue(widgetCell.isElementPresent(By.className("gwt-HTML"))); + + GridCellElement htmlCell = getGridElement().getHeaderCell(0, 2); + assertHTML("<b>Header (0,2)</b>", htmlCell); + } + + @Test + public void testDynamicallyChangingCellType() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 0", "Header Type", + "Widget Header"); + GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0); + assertTrue(widgetCell.isElementPresent(By.className("gwt-Button"))); + + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "HTML Header"); + GridCellElement htmlCell = getGridElement().getHeaderCell(0, 1); + assertHTML("<b>HTML Header</b>", htmlCell); + + selectMenuPath("Component", "Columns", "Column 2", "Header Type", + "Text Header"); + GridCellElement textCell = getGridElement().getHeaderCell(0, 2); + assertEquals("Text Header", textCell.getText()); + } + + @Test + public void testCellWidgetInteraction() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 0", "Header Type", + "Widget Header"); + GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0); + WebElement button = widgetCell.findElement(By.className("gwt-Button")); + + button.click(); + + assertEquals("Clicked", button.getText()); + } + + @Test + public void widgetInSortableCellInteraction() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 0", "Header Type", + "Widget Header"); + + selectMenuPath("Component", "Columns", "Column 0", "Sortable"); + + GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0); + WebElement button = widgetCell.findElement(By.className("gwt-Button")); + + assertNotEquals("Clicked", button.getText()); + + button.click(); + + assertEquals("Clicked", button.getText()); + } + + private void assertHeaderCount(int count) { + assertEquals("header count", count, getGridElement().getHeaderCount()); + } + + private boolean hasClassName(TestBenchElement element, String name) { + return Arrays.asList(element.getAttribute("class").split(" ")) + .contains(name); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java new file mode 100644 index 0000000000..cc801bf870 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertEquals; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures; + +/** + * Abstract base class for header and footer tests. + * + * @since + * @author Vaadin Ltd + */ +public abstract class GridStaticSectionTest extends GridBasicClientFeaturesTest { + + protected void assertHeaderTexts(int headerId, int rowIndex) { + int i = 0; + for (TestBenchElement cell : getGridElement().getHeaderCells(rowIndex)) { + + if (i % 3 == 0) { + assertText(String.format("Header (%d,%d)", headerId, i), cell); + } else if (i % 2 == 0) { + assertHTML(String.format("<b>Header (%d,%d)</b>", headerId, i), + cell); + } else { + assertHTML(String.format( + "<div class=\"gwt-HTML\">Header (%d,%d)</div>", + headerId, i), cell); + } + + i++; + } + assertEquals("number of header columns", GridBasicFeatures.COLUMNS, i); + } + + protected void assertFooterTexts(int footerId, int rowIndex) { + int i = 0; + for (TestBenchElement cell : getGridElement().getFooterCells(rowIndex)) { + if (i % 3 == 0) { + assertText(String.format("Footer (%d,%d)", footerId, i), cell); + } else if (i % 2 == 0) { + assertHTML(String.format("<b>Footer (%d,%d)</b>", footerId, i), + cell); + } else { + assertHTML(String.format( + "<div class=\"gwt-HTML\">Footer (%d,%d)</div>", + footerId, i), cell); + } + i++; + } + assertEquals("number of footer columns", GridBasicFeatures.COLUMNS, i); + } + + protected static void assertText(String text, TestBenchElement e) { + // TBE.getText returns "" if the element is scrolled out of view + assertEquals(text, e.getAttribute("innerHTML")); + } + + protected static void assertHTML(String text, TestBenchElement e) { + String html = e.getAttribute("innerHTML"); + + // IE 8 returns tags as upper case while other browsers do not, make the + // comparison non-casesensive + html = html.toLowerCase(); + text = text.toLowerCase(); + + // IE 8 returns attributes without quotes, make the comparison without + // quotes + html = html.replaceAll("\"", ""); + text = html.replaceAll("\"", ""); + + assertEquals(text, html); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java new file mode 100644 index 0000000000..67e974bd0a --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.testbench.By; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures; + +public class GridStylingTest extends GridStaticSectionTest { + + @Test + public void testGridPrimaryStyle() throws Exception { + openTestURL(); + + validateStylenames("v-grid"); + } + + @Test + public void testChangingPrimaryStyleName() throws Exception { + openTestURL(); + + selectMenuPath("Component", "State", "Primary Stylename", + "v-custom-style"); + + validateStylenames("v-custom-style"); + } + + private void validateStylenames(String stylename) { + + String classNames = getGridElement().getAttribute("class"); + assertEquals(stylename, classNames); + + classNames = getGridElement().getVerticalScroller().getAttribute( + "class"); + assertTrue(classNames.contains(stylename + "-scroller")); + assertTrue(classNames.contains(stylename + "-scroller-vertical")); + + classNames = getGridElement().getHorizontalScroller().getAttribute( + "class"); + assertTrue(classNames.contains(stylename + "-scroller")); + assertTrue(classNames.contains(stylename + "-scroller-horizontal")); + + classNames = getGridElement().getTableWrapper().getAttribute("class"); + assertEquals(stylename + "-tablewrapper", classNames); + + classNames = getGridElement().getHeader().getAttribute("class"); + assertEquals(stylename + "-header", classNames); + + for (int row = 0; row < getGridElement().getHeaderCount(); row++) { + classNames = getGridElement().getHeaderRow(row).getAttribute( + "class"); + assertEquals(stylename + "-row", classNames); + + for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) { + classNames = getGridElement().getHeaderCell(row, col) + .getAttribute("class"); + assertTrue(classNames.contains(stylename + "-cell")); + + if (row == 0 && col == 0) { + assertTrue(classNames, + classNames.contains(stylename + "-header-active")); + } + } + } + + classNames = getGridElement().getBody().getAttribute("class"); + assertEquals(stylename + "-body", classNames); + + int rowsInBody = getGridElement().getBody() + .findElements(By.tagName("tr")).size(); + for (int row = 0; row < rowsInBody; row++) { + classNames = getGridElement().getRow(row).getAttribute("class"); + assertTrue(classNames.contains(stylename + "-row")); + assertTrue(classNames.contains(stylename + "-row-has-data")); + + for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) { + classNames = getGridElement().getCell(row, col).getAttribute( + "class"); + assertTrue(classNames.contains(stylename + "-cell")); + + if (row == 0 && col == 0) { + assertTrue(classNames.contains(stylename + "-cell-active")); + } + } + } + + classNames = getGridElement().getFooter().getAttribute("class"); + assertEquals(stylename + "-footer", classNames); + + for (int row = 0; row < getGridElement().getFooterCount(); row++) { + classNames = getGridElement().getFooterRow(row).getAttribute( + "class"); + assertEquals(stylename + "-row", classNames); + + for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) { + classNames = getGridElement().getFooterCell(row, col) + .getAttribute("class"); + assertTrue(classNames.contains(stylename + "-cell")); + } + } + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java new file mode 100644 index 0000000000..0f9fd875d8 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.server; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.tests.components.grid.GridElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +public class GridKeyboardNavigationTest extends GridBasicFeaturesTest { + + @Test + public void testCellActiveOnClick() { + openTestURL(); + + GridElement grid = getGridElement(); + assertTrue("Body cell 0, 0 is not active on init.", grid.getCell(0, 0) + .isActive()); + grid.getCell(5, 2).click(); + assertFalse("Body cell 0, 0 was still active after clicking", grid + .getCell(0, 0).isActive()); + assertTrue("Body cell 5, 2 is not active after clicking", + grid.getCell(5, 2).isActive()); + } + + @Test + public void testCellNotActiveWhenRendererHandlesEvent() { + openTestURL(); + + GridElement grid = getGridElement(); + assertTrue("Body cell 0, 0 is not active on init.", grid.getCell(0, 0) + .isActive()); + grid.getHeaderCell(0, 3).click(); + assertFalse("Body cell 0, 0 is active after click on header.", grid + .getCell(0, 0).isActive()); + assertTrue("Header cell 0, 3 is not active after click on header.", + grid.getHeaderCell(0, 3).isActive()); + } + + @Test + public void testSimpleKeyboardNavigation() { + openTestURL(); + + GridElement grid = getGridElement(); + grid.getCell(0, 0).click(); + + new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform(); + assertTrue("Body cell 1, 0 is not active after keyboard navigation.", + grid.getCell(1, 0).isActive()); + + new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform(); + assertTrue("Body cell 1, 1 is not active after keyboard navigation.", + grid.getCell(1, 1).isActive()); + + int i; + for (i = 1; i < 40; ++i) { + new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform(); + } + + assertFalse("Grid has not scrolled with active cell", + isElementPresent(By.xpath("//td[text() = '(0, 0)']"))); + assertTrue("Active cell is not visible", + isElementPresent(By.xpath("//td[text() = '(" + i + ", 0)']"))); + assertTrue("Body cell " + i + ", 1 is not active", grid.getCell(i, 1) + .isActive()); + } + + @Test + public void testNavigateFromHeaderToBody() { + openTestURL(); + + GridElement grid = getGridElement(); + grid.scrollToRow(300); + new Actions(driver).moveToElement(grid.getHeaderCell(0, 7)).click() + .perform(); + grid.scrollToRow(280); + + assertTrue("Header cell is not active.", grid.getHeaderCell(0, 7) + .isActive()); + new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform(); + assertTrue("Body cell 280, 7 is not active", grid.getCell(280, 7) + .isActive()); + } + + @Test + public void testNavigationFromFooterToBody() { + openTestURL(); + + selectMenuPath("Component", "Footer", "Visible"); + + GridElement grid = getGridElement(); + grid.scrollToRow(300); + grid.getFooterCell(0, 2).click(); + + assertTrue("Footer cell is not active.", grid.getFooterCell(0, 2) + .isActive()); + new Actions(getDriver()).sendKeys(Keys.ARROW_UP).perform(); + assertTrue("Body cell 300, 2 is not active", grid.getCell(300, 2) + .isActive()); + } + + @Test + public void testNavigateBetweenHeaderAndBodyWithTab() { + openTestURL(); + + GridElement grid = getGridElement(); + grid.getCell(10, 2).click(); + + assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2) + .isActive()); + new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB) + .keyUp(Keys.SHIFT).perform(); + assertTrue("Header cell 0, 2 is not active", grid.getHeaderCell(0, 2) + .isActive()); + new Actions(getDriver()).sendKeys(Keys.TAB).perform(); + assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2) + .isActive()); + + // Navigate out of the Grid and try to navigate with arrow keys. + new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB) + .sendKeys(Keys.TAB).keyUp(Keys.SHIFT).sendKeys(Keys.ARROW_DOWN) + .perform(); + assertTrue("Header cell 0, 2 is not active", grid.getHeaderCell(0, 2) + .isActive()); + } + + @Test + public void testNavigateBetweenFooterAndBodyWithTab() { + openTestURL(); + + selectMenuPath("Component", "Footer", "Visible"); + + GridElement grid = getGridElement(); + grid.getCell(10, 2).click(); + + assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2) + .isActive()); + new Actions(getDriver()).sendKeys(Keys.TAB).perform(); + assertTrue("Footer cell 0, 2 is not active", grid.getFooterCell(0, 2) + .isActive()); + new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB) + .keyUp(Keys.SHIFT).perform(); + assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2) + .isActive()); + + // Navigate out of the Grid and try to navigate with arrow keys. + new Actions(getDriver()).sendKeys(Keys.TAB).sendKeys(Keys.TAB) + .sendKeys(Keys.ARROW_UP).perform(); + assertTrue("Footer cell 0, 2 is not active", grid.getFooterCell(0, 2) + .isActive()); + } + + @Test + public void testHomeEnd() throws Exception { + openTestURL(); + + getGridElement().getCell(100, 2).click(); + + new Actions(getDriver()).sendKeys(Keys.HOME).perform(); + assertTrue("First row is not visible", getGridElement().getCell(0, 2) + .isDisplayed()); + + new Actions(getDriver()).sendKeys(Keys.END).perform(); + assertTrue("Last row cell not visible", + getGridElement().getCell(GridBasicFeatures.ROWS - 1, 2) + .isDisplayed()); + } + + @Test + public void testPageUpPageDown() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Size", "HeightMode Row"); + + getGridElement().getCell(5, 2).click(); + + new Actions(getDriver()).sendKeys(Keys.PAGE_DOWN).perform(); + assertTrue("Row 5 did not remain active", getGridElement() + .getCell(5, 2).isActive()); + assertTrue("Row 20 did not become visible", + getGridElement().getCell(20, 2).isDisplayed()); + + new Actions(getDriver()).sendKeys(Keys.PAGE_DOWN).perform(); + assertTrue("Row 5 did not remain active", getGridElement() + .getCell(5, 2).isActive()); + assertTrue("Row 40 did not become visible", + getGridElement().getCell(40, 2).isDisplayed()); + + getGridElement().getCell(50, 2).click(); + + new Actions(getDriver()).sendKeys(Keys.PAGE_UP).perform(); + assertTrue("Row 50 did not remain active", + getGridElement().getCell(50, 2).isActive()); + assertTrue("Row 20 did not become visible", + getGridElement().getCell(20, 2).isDisplayed()); + + new Actions(getDriver()).sendKeys(Keys.PAGE_UP).perform(); + assertTrue("Row 50 did not remain active", + getGridElement().getCell(50, 2).isActive()); + assertTrue("Row 0 did not become visible", + getGridElement().getCell(0, 2).isDisplayed()); + + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java new file mode 100644 index 0000000000..6e2ac91df2 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.server; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.components.grid.GridElement; +import com.vaadin.tests.components.grid.GridElement.GridRowElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +public class GridSelectionTest extends GridBasicFeaturesTest { + + @Test + public void testSelectOnOff() throws Exception { + openTestURL(); + + setSelectionModelMulti(); + + assertFalse("row shouldn't start out as selected", getRow(0) + .isSelected()); + toggleFirstRowSelection(); + assertTrue("row should become selected", getRow(0).isSelected()); + toggleFirstRowSelection(); + assertFalse("row shouldn't remain selected", getRow(0).isSelected()); + } + + @Test + public void testSelectOnScrollOffScroll() throws Exception { + openTestURL(); + + setSelectionModelMulti(); + + assertFalse("row shouldn't start out as selected", getRow(0) + .isSelected()); + toggleFirstRowSelection(); + assertTrue("row should become selected", getRow(0).isSelected()); + + scrollGridVerticallyTo(10000); // make sure the row is out of cache + scrollGridVerticallyTo(0); // scroll it back into view + + assertTrue("row should still be selected when scrolling " + + "back into view", getRow(0).isSelected()); + } + + @Test + public void testSelectScrollOnScrollOff() throws Exception { + openTestURL(); + + setSelectionModelMulti(); + + assertFalse("row shouldn't start out as selected", getRow(0) + .isSelected()); + + scrollGridVerticallyTo(10000); // make sure the row is out of cache + toggleFirstRowSelection(); + + scrollGridVerticallyTo(0); // scroll it back into view + assertTrue("row should still be selected when scrolling " + + "back into view", getRow(0).isSelected()); + + toggleFirstRowSelection(); + assertFalse("row shouldn't remain selected", getRow(0).isSelected()); + } + + @Test + public void testSelectScrollOnOffScroll() throws Exception { + openTestURL(); + + setSelectionModelMulti(); + + assertFalse("row shouldn't start out as selected", getRow(0) + .isSelected()); + + scrollGridVerticallyTo(10000); // make sure the row is out of cache + toggleFirstRowSelection(); + toggleFirstRowSelection(); + + scrollGridVerticallyTo(0); // make sure the row is out of cache + assertFalse("row shouldn't be selected when scrolling " + + "back into view", getRow(0).isSelected()); + } + + @Test + public void testSingleSelectionUpdatesFromServer() { + openTestURL(); + setSelectionModelSingle(); + + GridElement grid = getGridElement(); + assertFalse("First row was selected from start", grid.getRow(0) + .isSelected()); + toggleFirstRowSelection(); + assertTrue("First row was not selected.", getRow(0).isSelected()); + grid.getCell(5, 0).click(); + assertTrue("Fifth row was not selected.", getRow(5).isSelected()); + assertFalse("First row was still selected.", getRow(0).isSelected()); + grid.getCell(0, 0).click(); + toggleFirstRowSelection(); + assertFalse("First row was still selected.", getRow(0).isSelected()); + assertFalse("Fifth row was still selected.", getRow(5).isSelected()); + + grid.scrollToRow(600); + grid.getCell(595, 0).click(); + assertTrue("Row 595 was not selected.", getRow(595).isSelected()); + toggleFirstRowSelection(); + assertFalse("Row 595 was still selected.", getRow(595).isSelected()); + assertTrue("First row was not selected.", getRow(0).isSelected()); + } + + @Test + public void testKeyboardSelection() { + openTestURL(); + setSelectionModelMulti(); + + GridElement grid = getGridElement(); + grid.getCell(3, 1).click(); + new Actions(getDriver()).sendKeys(Keys.SPACE).perform(); + + assertTrue("Grid row 3 was not selected with space key.", grid + .getRow(3).isSelected()); + + new Actions(getDriver()).sendKeys(Keys.SPACE).perform(); + + assertTrue("Grid row 3 was not deselected with space key.", !grid + .getRow(3).isSelected()); + + grid.scrollToRow(500); + + new Actions(getDriver()).sendKeys(Keys.SPACE).perform(); + + assertTrue("Grid row 3 was not selected with space key.", grid + .getRow(3).isSelected()); + + } + + private void setSelectionModelMulti() { + selectMenuPath("Component", "State", "Selection mode", "multi"); + } + + private void setSelectionModelSingle() { + selectMenuPath("Component", "State", "Selection mode", "single"); + } + + @SuppressWarnings("static-method") + private boolean isSelected(TestBenchElement row) { + /* + * FIXME We probably should get a GridRow instead of a plain + * TestBenchElement, that has an "isSelected" thing integrated. (henrik + * paul 26.6.2014) + */ + return row.getAttribute("class").contains("-row-selected"); + } + + private void toggleFirstRowSelection() { + selectMenuPath("Component", "Body rows", "Select first row"); + } + + private GridRowElement getRow(int i) { + return getGridElement().getRow(i); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java new file mode 100644 index 0000000000..024be65e83 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.tests.components.grid.GridElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +public class GridSortingTest extends GridBasicFeaturesTest { + + @Test + public void testProgrammaticSorting() throws IOException { + openTestURL(); + + GridElement grid = getGridElement(); + + // Sorting by column 9 is sorting by row index that is represented as a + // String. + // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0) + sortBy("Column 9, DESC"); + + // Verify that programmatic sorting calls are identified as originating + // from API + assertEquals("3. Sort order: [Column 9 DESCENDING] by API", + getLogRow(0)); + + assertTrue("Column 9 should have the sort-desc stylename", grid + .getHeaderCell(0, 9).getAttribute("class") + .contains("sort-desc")); + + String row = ""; + for (int i = 0; i < 3; ++i) { + row += "9"; + assertEquals( + "Grid is not sorted by Column 9 using descending direction.", + "(" + row + ", 0)", grid.getCell(i, 0).getText()); + } + + // Column 10 is random numbers from Random with seed 13334 + sortBy("Column 10, ASC"); + + assertFalse( + "Column 9 should no longer have the sort-desc stylename", + grid.getHeaderCell(0, 9).getAttribute("class") + .contains("sort-desc")); + assertTrue("Column 10 should have the sort-asc stylename", grid + .getHeaderCell(0, 10).getAttribute("class") + .contains("sort-asc")); + + // Not cleaning up correctly causes exceptions when scrolling. + grid.scrollToRow(50); + assertFalse("Scrolling caused and exception when shuffled.", + getLogRow(0).contains("Exception")); + + for (int i = 0; i < 5; ++i) { + assertGreater( + "Grid is not sorted by Column 10 using ascending direction", + Integer.parseInt(grid.getCell(i + 1, 10).getText()), + Integer.parseInt(grid.getCell(i, 10).getText())); + + } + + // Column 7 is row index as a number. Last three row are original rows + // 2, 1 and 0. + sortBy("Column 7, DESC"); + for (int i = 0; i < 3; ++i) { + assertEquals( + "Grid is not sorted by Column 7 using descending direction", + "(" + i + ", 0)", + grid.getCell(GridBasicFeatures.ROWS - (i + 1), 0).getText()); + } + + assertFalse( + "Column 10 should no longer have the sort-asc stylename", + grid.getHeaderCell(0, 10).getAttribute("class") + .contains("sort-asc")); + assertTrue("Column 7 should have the sort-desc stylename", grid + .getHeaderCell(0, 7).getAttribute("class") + .contains("sort-desc")); + + } + + @Test + public void testUserSorting() throws InterruptedException { + openTestURL(); + + GridElement grid = getGridElement(); + + // Sorting by column 9 is sorting by row index that is represented as a + // String. + // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0) + + // Click header twice to sort descending + grid.getHeaderCell(0, 9).click(); + grid.getHeaderCell(0, 9).click(); + String row = ""; + for (int i = 0; i < 3; ++i) { + row += "9"; + assertEquals( + "Grid is not sorted by Column 9 using descending direction.", + "(" + row + ", 0)", grid.getCell(i, 0).getText()); + } + + assertEquals("2. Sort order: [Column 9 ASCENDING] by USER", + getLogRow(2)); + assertEquals("4. Sort order: [Column 9 DESCENDING] by USER", + getLogRow(0)); + + // Column 10 is random numbers from Random with seed 13334 + // Click header to sort ascending + grid.getHeaderCell(0, 10).click(); + + assertEquals("6. Sort order: [Column 10 ASCENDING] by USER", + getLogRow(0)); + + // Not cleaning up correctly causes exceptions when scrolling. + grid.scrollToRow(50); + assertFalse("Scrolling caused and exception when shuffled.", + getLogRow(0).contains("Exception")); + + for (int i = 0; i < 5; ++i) { + assertGreater( + "Grid is not sorted by Column 10 using ascending direction", + Integer.parseInt(grid.getCell(i + 1, 10).getText()), + Integer.parseInt(grid.getCell(i, 10).getText())); + + } + + // Column 7 is row index as a number. Last three row are original rows + // 2, 1 and 0. + // Click header twice to sort descending + grid.getHeaderCell(0, 7).click(); + grid.getHeaderCell(0, 7).click(); + for (int i = 0; i < 3; ++i) { + assertEquals( + "Grid is not sorted by Column 7 using descending direction", + "(" + i + ", 0)", + grid.getCell(GridBasicFeatures.ROWS - (i + 1), 0).getText()); + } + + assertEquals("9. Sort order: [Column 7 ASCENDING] by USER", + getLogRow(3)); + assertEquals("11. Sort order: [Column 7 DESCENDING] by USER", + getLogRow(1)); + } + + @Test + public void testUserMultiColumnSorting() { + openTestURL(); + + getGridElement().getHeaderCell(0, 0).click(); + new Actions(driver).keyDown(Keys.SHIFT).perform(); + getGridElement().getHeaderCell(0, 11).click(); + new Actions(driver).keyUp(Keys.SHIFT).perform(); + + String prev = getGridElement().getCell(0, 11).getAttribute("innerHTML"); + for (int i = 1; i <= 6; ++i) { + assertEquals("Column 11 should contain same values.", prev, + getGridElement().getCell(i, 11).getAttribute("innerHTML")); + } + + prev = getGridElement().getCell(0, 0).getText(); + for (int i = 1; i <= 6; ++i) { + assertTrue( + "Grid is not sorted by column 0.", + prev.compareTo(getGridElement().getCell(i, 0).getText()) < 0); + } + + } + + private void sortBy(String column) { + selectMenuPath("Component", "State", "Sort by column", column); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java new file mode 100644 index 0000000000..19a68a87f4 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.server; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +public class GridStaticSectionComponentTest extends GridBasicFeaturesTest { + + @Test + public void testNativeButtonInHeader() throws IOException { + openTestURL(); + + selectMenuPath("Component", "Columns", "Column 1", "Header Type", + "Widget Header"); + + getGridElement().$(ButtonElement.class).first().click(); + + // Clicking also triggers sorting + assertEquals("2. Button clicked!", getLogRow(2)); + } + + @Test + public void testNativeButtonInFooter() throws IOException { + openTestURL(); + + selectMenuPath("Component", "Footer", "Visible"); + selectMenuPath("Component", "Footer", "Append row"); + selectMenuPath("Component", "Columns", "Column 1", "Footer Type", + "Widget Footer"); + + getGridElement().$(ButtonElement.class).first().click(); + + assertEquals("4. Button clicked!", getLogRow(0)); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java new file mode 100644 index 0000000000..7c5607d4e6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java @@ -0,0 +1,229 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.server; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +public class GridStructureTest extends GridBasicFeaturesTest { + + @Test + public void testHidingColumn() throws Exception { + openTestURL(); + + // Column 0 should be visible + List<TestBenchElement> cells = getGridHeaderRowCells(); + assertEquals("Column 0", cells.get(0).getText()); + + // Hide column 0 + selectMenuPath("Component", "Columns", "Column 0", "Visible"); + + // Column 1 should now be the first cell + cells = getGridHeaderRowCells(); + assertEquals("Column 1", cells.get(0).getText()); + } + + @Test + public void testRemovingColumn() throws Exception { + openTestURL(); + + // Column 0 should be visible + List<TestBenchElement> cells = getGridHeaderRowCells(); + assertEquals("Column 0", cells.get(0).getText()); + + // Hide column 0 + selectMenuPath("Component", "Columns", "Column 0", "Remove"); + + // Column 1 should now be the first cell + cells = getGridHeaderRowCells(); + assertEquals("Column 1", cells.get(0).getText()); + } + + @Test + public void testDataLoadingAfterRowRemoval() throws Exception { + openTestURL(); + + // Remove columns 2,3,4 + selectMenuPath("Component", "Columns", "Column 2", "Remove"); + selectMenuPath("Component", "Columns", "Column 3", "Remove"); + selectMenuPath("Component", "Columns", "Column 4", "Remove"); + + // Scroll so new data is lazy loaded + scrollGridVerticallyTo(1000); + + // Let lazy loading do its job + sleep(1000); + + // Check that row is loaded + assertThat(getGridElement().getCell(11, 0).getText(), not("...")); + } + + @Test + public void testFreezingColumn() throws Exception { + openTestURL(); + + // Freeze column 2 + selectMenuPath("Component", "Columns", "Column 2", "Freeze"); + + WebElement cell = getGridElement().getCell(0, 0); + assertTrue(cell.getAttribute("class").contains("frozen")); + + cell = getGridElement().getCell(0, 1); + assertTrue(cell.getAttribute("class").contains("frozen")); + } + + @Test + public void testInitialColumnWidths() throws Exception { + openTestURL(); + + WebElement cell = getGridElement().getCell(0, 0); + assertEquals(100, cell.getSize().getWidth()); + + cell = getGridElement().getCell(0, 1); + assertEquals(150, cell.getSize().getWidth()); + + cell = getGridElement().getCell(0, 2); + assertEquals(200, cell.getSize().getWidth()); + } + + @Test + public void testColumnWidths() throws Exception { + openTestURL(); + + // Default column width is 100px + WebElement cell = getGridElement().getCell(0, 0); + assertEquals(100, cell.getSize().getWidth()); + + // Set first column to be 200px wide + selectMenuPath("Component", "Columns", "Column 0", "Column 0 Width", + "200px"); + + cell = getGridElement().getCell(0, 0); + assertEquals(200, cell.getSize().getWidth()); + + // Set second column to be 150px wide + selectMenuPath("Component", "Columns", "Column 1", "Column 1 Width", + "150px"); + cell = getGridElement().getCell(0, 1); + assertEquals(150, cell.getSize().getWidth()); + + // Set first column to be auto sized (defaults to 100px currently) + selectMenuPath("Component", "Columns", "Column 0", "Column 0 Width", + "Auto"); + + cell = getGridElement().getCell(0, 0); + assertEquals(100, cell.getSize().getWidth()); + } + + @Test + public void testPrimaryStyleNames() throws Exception { + openTestURL(); + + // v-grid is default primary style namea + assertPrimaryStylename("v-grid"); + + selectMenuPath("Component", "State", "Primary style name", + "v-escalator"); + assertPrimaryStylename("v-escalator"); + + selectMenuPath("Component", "State", "Primary style name", "my-grid"); + assertPrimaryStylename("my-grid"); + + selectMenuPath("Component", "State", "Primary style name", "v-grid"); + assertPrimaryStylename("v-grid"); + } + + /** + * Test that the current view is updated when a server-side container change + * occurs (without scrolling back and forth) + */ + @Test + public void testItemSetChangeEvent() throws Exception { + openTestURL(); + + final By newRow = By.xpath("//td[text()='newcell: 0']"); + + assertTrue("Unexpected initial state", !isElementPresent(newRow)); + + selectMenuPath("Component", "Body rows", "Add first row"); + assertTrue("Add row failed", isElementPresent(newRow)); + + selectMenuPath("Component", "Body rows", "Remove first row"); + assertTrue("Remove row failed", !isElementPresent(newRow)); + } + + /** + * Test that the current view is updated when a property's value is reflect + * to the client, when the value is modified server-side. + */ + @Test + public void testPropertyValueChangeEvent() throws Exception { + openTestURL(); + + assertEquals("Unexpected cell initial state", "(0, 0)", + getGridElement().getCell(0, 0).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getItemProperty)"); + assertEquals("(First) modification with getItemProperty failed", + "modified: 0", getGridElement().getCell(0, 0).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getContainerProperty)"); + assertEquals("(Second) modification with getItemProperty failed", + "modified: Column 0", getGridElement().getCell(0, 0).getText()); + } + + @Test + public void testRemovingAllItems() throws Exception { + openTestURL(); + + selectMenuPath("Component", "Body rows", "Remove all rows"); + + assertEquals(0, getGridElement().findElement(By.tagName("tbody")) + .findElements(By.tagName("tr")).size()); + } + + private void assertPrimaryStylename(String stylename) { + assertTrue(getGridElement().getAttribute("class").contains(stylename)); + + String tableWrapperStyleName = getGridElement().getTableWrapper() + .getAttribute("class"); + assertTrue(tableWrapperStyleName.contains(stylename + "-tablewrapper")); + + String hscrollStyleName = getGridElement().getHorizontalScroller() + .getAttribute("class"); + assertTrue(hscrollStyleName.contains(stylename + "-scroller")); + assertTrue(hscrollStyleName + .contains(stylename + "-scroller-horizontal")); + + String vscrollStyleName = getGridElement().getVerticalScroller() + .getAttribute("class"); + assertTrue(vscrollStyleName.contains(stylename + "-scroller")); + assertTrue(vscrollStyleName.contains(stylename + "-scroller-vertical")); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml b/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml index 2c25c54e04..d23903f9db 100644 --- a/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml +++ b/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml @@ -4,6 +4,8 @@ <!-- Inherit the DefaultWidgetSet --> <inherits name="com.vaadin.DefaultWidgetSet" /> + <inherits name="com.google.gwt.user.theme.standard.Standard" /> + <replace-with class="com.vaadin.tests.widgetset.client.CustomUIConnector"> <when-type-is class="com.vaadin.client.ui.ui.UIConnector" /> </replace-with> diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java new file mode 100644 index 0000000000..b0841b69fb --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeatures.GridTestComponent; + +/** + * Connector for the GridClientBasicFeatures ApplicationWidget + * + * @since + * @author Vaadin Ltd + */ +@Connect(GridTestComponent.class) +public class GridBasicClientFeaturesConnector extends + AbstractComponentConnector { + + @Override + public GridBasicClientFeaturesWidget getWidget() { + return (GridBasicClientFeaturesWidget) super.getWidget(); + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java new file mode 100644 index 0000000000..a7210236d4 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -0,0 +1,755 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ui.VLabel; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent; +import com.vaadin.client.ui.grid.Grid.SelectionMode; +import com.vaadin.client.ui.grid.GridColumn; +import com.vaadin.client.ui.grid.GridFooter; +import com.vaadin.client.ui.grid.GridFooter.FooterRow; +import com.vaadin.client.ui.grid.GridHeader; +import com.vaadin.client.ui.grid.GridHeader.HeaderRow; +import com.vaadin.client.ui.grid.Renderer; +import com.vaadin.client.ui.grid.datasources.ListDataSource; +import com.vaadin.client.ui.grid.datasources.ListSorter; +import com.vaadin.client.ui.grid.keyevents.BodyKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.BodyKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.BodyKeyUpHandler; +import com.vaadin.client.ui.grid.keyevents.FooterKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.FooterKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.FooterKeyUpHandler; +import com.vaadin.client.ui.grid.keyevents.GridKeyDownEvent; +import com.vaadin.client.ui.grid.keyevents.GridKeyPressEvent; +import com.vaadin.client.ui.grid.keyevents.GridKeyUpEvent; +import com.vaadin.client.ui.grid.keyevents.HeaderKeyDownHandler; +import com.vaadin.client.ui.grid.keyevents.HeaderKeyPressHandler; +import com.vaadin.client.ui.grid.keyevents.HeaderKeyUpHandler; +import com.vaadin.client.ui.grid.renderers.DateRenderer; +import com.vaadin.client.ui.grid.renderers.HtmlRenderer; +import com.vaadin.client.ui.grid.renderers.NumberRenderer; +import com.vaadin.client.ui.grid.renderers.TextRenderer; +import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget.Data; + +/** + * Grid basic client features test application. + * + * @since + * @author Vaadin Ltd + */ +public class GridBasicClientFeaturesWidget extends + PureGWTTestApplication<Grid<List<Data>>> { + + public static enum Renderers { + TEXT_RENDERER, HTML_RENDERER, NUMBER_RENDERER, DATE_RENDERER; + } + + private static final int MANUALLY_FORMATTED_COLUMNS = 5; + public static final int COLUMNS = 12; + public static final int ROWS = 1000; + + private final Grid<List<Data>> grid; + private final List<List<Data>> data; + private final ListDataSource<List<Data>> ds; + private final ListSorter<List<Data>> sorter; + + /** + * Our basic data object + */ + public final static class Data { + Object value; + } + + /** + * Convenience method for creating a list of Data objects to be used as a + * Row in the data source + * + * @param cols + * number of columns (items) to include in the row + * @return + */ + private List<Data> createDataRow(int cols) { + List<Data> list = new ArrayList<Data>(cols); + for (int i = 0; i < cols; ++i) { + list.add(new Data()); + } + data.add(list); + return list; + } + + @SuppressWarnings("unchecked") + public GridBasicClientFeaturesWidget() { + super(new Grid<List<Data>>()); + + // Initialize data source + data = new ArrayList<List<Data>>(); + { + Random rand = new Random(); + rand.setSeed(13334); + long timestamp = 0; + for (int row = 0; row < ROWS; row++) { + + List<Data> datarow = createDataRow(COLUMNS); + Data d; + + int col = 0; + for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; ++col) { + d = datarow.get(col); + d.value = "(" + row + ", " + col + ")"; + } + + d = datarow.get(col++); + d.value = Integer.valueOf(row); + + d = datarow.get(col++); + d.value = new Date(timestamp); + timestamp += 91250000; // a bit over a day, just to get + // variation + + d = datarow.get(col++); + d.value = "<b>" + row + "</b>"; + + d = datarow.get(col++); + d.value = Integer.valueOf(rand.nextInt()); + + d = datarow.get(col++); + d.value = Integer.valueOf(rand.nextInt(5)); + } + } + + ds = new ListDataSource<List<Data>>(data); + grid = getTestedWidget(); + grid.getElement().setId("testComponent"); + grid.setDataSource(ds); + grid.setSelectionMode(SelectionMode.NONE); + + sorter = new ListSorter<List<Data>>(grid); + + // Create a bunch of grid columns + + // Data source layout: + // text (String) * (COLUMNS - MANUALLY_FORMATTED_COLUMNS + 1) | + // rownumber (Integer) | some date (Date) | row number as HTML (String) + // | random value (Integer) + + int col = 0; + + // Text times COLUMNS - MANUALLY_FORMATTED_COLUMNS + for (col = 0; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; ++col) { + + final int c = col; + + GridColumn<String, List<Data>> column = new GridColumn<String, List<Data>>( + createRenderer(Renderers.TEXT_RENDERER)) { + @Override + public String getValue(List<Data> row) { + return (String) row.get(c).value; + } + }; + + column.setWidth(50 + c * 25); + + grid.addColumn(column); + + } + + // Integer row number + { + final int c = col++; + grid.addColumn(new GridColumn<Integer, List<Data>>( + createRenderer(Renderers.NUMBER_RENDERER)) { + @Override + public Integer getValue(List<Data> row) { + return (Integer) row.get(c).value; + } + }); + } + + // Some date + { + final int c = col++; + grid.addColumn(new GridColumn<Date, List<Data>>( + createRenderer(Renderers.DATE_RENDERER)) { + @Override + public Date getValue(List<Data> row) { + return (Date) row.get(c).value; + } + }); + } + + // Row number as a HTML string + { + final int c = col++; + grid.addColumn(new GridColumn<String, List<Data>>( + createRenderer(Renderers.HTML_RENDERER)) { + @Override + public String getValue(List<Data> row) { + return (String) row.get(c).value; + } + }); + } + + // Random integer value + { + final int c = col++; + grid.addColumn(new GridColumn<Integer, List<Data>>( + createRenderer(Renderers.NUMBER_RENDERER)) { + @Override + public Integer getValue(List<Data> row) { + return (Integer) row.get(c).value; + } + }); + } + + // Random integer value between 0 and 5 + { + final int c = col++; + grid.addColumn(new GridColumn<Integer, List<Data>>( + createRenderer(Renderers.NUMBER_RENDERER)) { + @Override + public Integer getValue(List<Data> row) { + return (Integer) row.get(c).value; + } + }); + } + + setHeaderTexts(grid.getHeader().getRow(0)); + + // + // Populate the menu + // + + createStateMenu(); + createColumnsMenu(); + createHeaderMenu(); + createFooterMenu(); + + grid.getElement().getStyle().setZIndex(0); + addNorth(grid, 400); + + createKeyHandlers(); + } + + private void createStateMenu() { + String[] selectionModePath = { "Component", "State", "Selection mode" }; + String[] primaryStyleNamePath = { "Component", "State", + "Primary Stylename" }; + + addMenuCommand("multi", new ScheduledCommand() { + @Override + public void execute() { + grid.setSelectionMode(SelectionMode.MULTI); + } + }, selectionModePath); + + addMenuCommand("single", new ScheduledCommand() { + @Override + public void execute() { + grid.setSelectionMode(SelectionMode.SINGLE); + } + }, selectionModePath); + + addMenuCommand("none", new ScheduledCommand() { + @Override + public void execute() { + grid.setSelectionMode(SelectionMode.NONE); + } + }, selectionModePath); + + addMenuCommand("v-grid", new ScheduledCommand() { + @Override + public void execute() { + grid.setStylePrimaryName("v-grid"); + + } + }, primaryStyleNamePath); + + addMenuCommand("v-escalator", new ScheduledCommand() { + @Override + public void execute() { + grid.setStylePrimaryName("v-escalator"); + + } + }, primaryStyleNamePath); + + addMenuCommand("v-custom-style", new ScheduledCommand() { + @Override + public void execute() { + grid.setStylePrimaryName("v-custom-style"); + + } + }, primaryStyleNamePath); + + } + + private void createColumnsMenu() { + + for (int i = 0; i < COLUMNS; i++) { + final int index = i; + addMenuCommand("Visible", new ScheduledCommand() { + @Override + public void execute() { + grid.getColumn(index).setVisible( + !grid.getColumn(index).isVisible()); + } + }, "Component", "Columns", "Column " + i); + addMenuCommand("Sortable", new ScheduledCommand() { + @Override + public void execute() { + grid.getColumn(index).setSortable( + !grid.getColumn(index).isSortable()); + } + }, "Component", "Columns", "Column " + i); + + addMenuCommand("auto", new ScheduledCommand() { + @Override + public void execute() { + grid.getColumn(index).setWidth(-1); + } + }, "Component", "Columns", "Column " + i, "Width"); + addMenuCommand("50px", new ScheduledCommand() { + @Override + public void execute() { + grid.getColumn(index).setWidth(50); + } + }, "Component", "Columns", "Column " + i, "Width"); + addMenuCommand("200px", new ScheduledCommand() { + @Override + public void execute() { + grid.getColumn(index).setWidth(200); + } + }, "Component", "Columns", "Column " + i, "Width"); + + // Header types + addMenuCommand("Text Header", new ScheduledCommand() { + @Override + public void execute() { + grid.getHeader().getRow(0).getCell(index) + .setText("Text Header"); + } + }, "Component", "Columns", "Column " + i, "Header Type"); + addMenuCommand("HTML Header", new ScheduledCommand() { + @Override + public void execute() { + grid.getHeader().getRow(0).getCell(index) + .setHtml("<b>HTML Header</b>"); + } + }, "Component", "Columns", "Column " + i, "Header Type"); + addMenuCommand("Widget Header", new ScheduledCommand() { + @Override + public void execute() { + final Button button = new Button("Button Header"); + button.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + button.setText("Clicked"); + } + }); + grid.getHeader().getRow(0).getCell(index).setWidget(button); + } + }, "Component", "Columns", "Column " + i, "Header Type"); + + // Footer types + addMenuCommand("Text Footer", new ScheduledCommand() { + @Override + public void execute() { + grid.getFooter().getRow(0).getCell(index) + .setText("Text Footer"); + } + }, "Component", "Columns", "Column " + i, "Footer Type"); + addMenuCommand("HTML Footer", new ScheduledCommand() { + @Override + public void execute() { + grid.getFooter().getRow(0).getCell(index) + .setHtml("<b>HTML Footer</b>"); + } + }, "Component", "Columns", "Column " + i, "Footer Type"); + addMenuCommand("Widget Footer", new ScheduledCommand() { + @Override + public void execute() { + final Button button = new Button("Button Footer"); + button.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + button.setText("Clicked"); + } + }); + grid.getFooter().getRow(0).getCell(index).setWidget(button); + } + }, "Component", "Columns", "Column " + i, "Footer Type"); + } + } + + private int headerCounter = 0; + private int footerCounter = 0; + + private void setHeaderTexts(HeaderRow row) { + for (int i = 0; i < COLUMNS; ++i) { + String caption = "Header (" + headerCounter + "," + i + ")"; + + // Lets use some different cell types + if (i % 3 == 0) { + row.getCell(i).setText(caption); + } else if (i % 2 == 0) { + row.getCell(i).setHtml("<b>" + caption + "</b>"); + } else { + row.getCell(i).setWidget(new HTML(caption)); + } + } + headerCounter++; + } + + private void setFooterTexts(FooterRow row) { + for (int i = 0; i < COLUMNS; ++i) { + String caption = "Footer (" + footerCounter + "," + i + ")"; + + // Lets use some different cell types + if (i % 3 == 0) { + row.getCell(i).setText(caption); + } else if (i % 2 == 0) { + row.getCell(i).setHtml("<b>" + caption + "</b>"); + } else { + row.getCell(i).setWidget(new HTML(caption)); + } + } + footerCounter++; + } + + private void createHeaderMenu() { + final GridHeader header = grid.getHeader(); + final String[] menuPath = { "Component", "Header" }; + + addMenuCommand("Visible", new ScheduledCommand() { + @Override + public void execute() { + header.setVisible(!header.isVisible()); + } + }, menuPath); + + addMenuCommand("Top", new ScheduledCommand() { + @Override + public void execute() { + header.setDefaultRow(header.getRow(0)); + } + }, "Component", "Header", "Default row"); + addMenuCommand("Bottom", new ScheduledCommand() { + @Override + public void execute() { + header.setDefaultRow(header.getRow(header.getRowCount() - 1)); + } + }, "Component", "Header", "Default row"); + addMenuCommand("Unset", new ScheduledCommand() { + @Override + public void execute() { + header.setDefaultRow(null); + } + }, "Component", "Header", "Default row"); + + addMenuCommand("Prepend row", new ScheduledCommand() { + @Override + public void execute() { + configureHeaderRow(header.prependRow()); + } + }, menuPath); + addMenuCommand("Append row", new ScheduledCommand() { + @Override + public void execute() { + configureHeaderRow(header.appendRow()); + } + }, menuPath); + addMenuCommand("Remove top row", new ScheduledCommand() { + @Override + public void execute() { + header.removeRow(0); + } + }, menuPath); + addMenuCommand("Remove bottom row", new ScheduledCommand() { + @Override + public void execute() { + header.removeRow(header.getRowCount() - 1); + } + }, menuPath); + + } + + private void configureHeaderRow(final HeaderRow row) { + final GridHeader header = grid.getHeader(); + setHeaderTexts(row); + String rowTitle = "Row " + header.getRowCount(); + final String[] menuPath = { "Component", "Header", rowTitle }; + + addMenuCommand("Join column cells 0, 1", new ScheduledCommand() { + + @Override + public void execute() { + row.join(row.getCell(0), row.getCell(1)); + + } + }, menuPath); + + addMenuCommand("Join columns 1, 2", new ScheduledCommand() { + + @Override + public void execute() { + row.join(grid.getColumn(1), grid.getColumn(2)); + + } + }, menuPath); + + addMenuCommand("Join columns 3, 4, 5", new ScheduledCommand() { + + @Override + public void execute() { + row.join(grid.getColumn(3), grid.getColumn(4), + grid.getColumn(5)); + + } + }, menuPath); + + addMenuCommand("Join all columns", new ScheduledCommand() { + + @Override + public void execute() { + row.join(grid.getColumns().toArray( + new GridColumn[grid.getColumnCount()])); + + } + }, menuPath); + } + + private void createFooterMenu() { + final GridFooter footer = grid.getFooter(); + final String[] menuPath = { "Component", "Footer" }; + + addMenuCommand("Visible", new ScheduledCommand() { + @Override + public void execute() { + footer.setVisible(!footer.isVisible()); + } + }, menuPath); + + addMenuCommand("Prepend row", new ScheduledCommand() { + @Override + public void execute() { + configureFooterRow(footer.prependRow()); + } + }, menuPath); + addMenuCommand("Append row", new ScheduledCommand() { + @Override + public void execute() { + configureFooterRow(footer.appendRow()); + } + }, menuPath); + addMenuCommand("Remove top row", new ScheduledCommand() { + @Override + public void execute() { + footer.removeRow(0); + } + }, menuPath); + addMenuCommand("Remove bottom row", new ScheduledCommand() { + @Override + public void execute() { + assert footer.getRowCount() > 0; + footer.removeRow(footer.getRowCount() - 1); + } + }, menuPath); + } + + private void configureFooterRow(final FooterRow row) { + final GridFooter footer = grid.getFooter(); + setFooterTexts(row); + String rowTitle = "Row " + footer.getRowCount(); + final String[] menuPath = { "Component", "Footer", rowTitle }; + + addMenuCommand("Join column cells 0, 1", new ScheduledCommand() { + + @Override + public void execute() { + row.join(row.getCell(0), row.getCell(1)); + + } + }, menuPath); + + addMenuCommand("Join columns 1, 2", new ScheduledCommand() { + + @Override + public void execute() { + row.join(grid.getColumn(1), grid.getColumn(2)); + + } + }, menuPath); + + addMenuCommand("Join all columns", new ScheduledCommand() { + + @Override + public void execute() { + row.join(grid.getColumns().toArray( + new GridColumn[grid.getColumnCount()])); + + } + }, menuPath); + } + + /** + * Creates a renderer for a {@link Renderers} + */ + @SuppressWarnings("rawtypes") + private final Renderer createRenderer(Renderers renderer) { + switch (renderer) { + case TEXT_RENDERER: + return new TextRenderer(); + + case HTML_RENDERER: + return new HtmlRenderer() { + + @Override + public void render(FlyweightCell cell, String htmlString) { + super.render(cell, "<b>" + htmlString + "</b>"); + } + }; + + case NUMBER_RENDERER: + return new NumberRenderer<Integer>(); + + case DATE_RENDERER: + return new DateRenderer(); + + default: + return new TextRenderer(); + } + } + + /** + * Creates a collection of handlers for all the grid key events + */ + private void createKeyHandlers() { + final List<VLabel> labels = new ArrayList<VLabel>(); + for (int i = 0; i < 9; ++i) { + VLabel tmp = new VLabel(); + addNorth(tmp, 20); + labels.add(tmp); + } + + // Key Down Events + grid.addKeyDownHandler(new BodyKeyDownHandler<List<Data>>() { + private final VLabel label = labels.get(0); + + @Override + public void onKeyDown(GridKeyDownEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + grid.addKeyDownHandler(new HeaderKeyDownHandler<List<Data>>() { + private final VLabel label = labels.get(1); + + @Override + public void onKeyDown(GridKeyDownEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + grid.addKeyDownHandler(new FooterKeyDownHandler<List<Data>>() { + private final VLabel label = labels.get(2); + + @Override + public void onKeyDown(GridKeyDownEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + // Key Up Events + grid.addKeyUpHandler(new BodyKeyUpHandler<List<Data>>() { + private final VLabel label = labels.get(3); + + @Override + public void onKeyUp(GridKeyUpEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + grid.addKeyUpHandler(new HeaderKeyUpHandler<List<Data>>() { + private final VLabel label = labels.get(4); + + @Override + public void onKeyUp(GridKeyUpEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + grid.addKeyUpHandler(new FooterKeyUpHandler<List<Data>>() { + private final VLabel label = labels.get(5); + + @Override + public void onKeyUp(GridKeyUpEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + // Key Press Events + grid.addKeyPressHandler(new BodyKeyPressHandler<List<Data>>() { + private final VLabel label = labels.get(6); + + @Override + public void onKeyPress(GridKeyPressEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + grid.addKeyPressHandler(new HeaderKeyPressHandler<List<Data>>() { + private final VLabel label = labels.get(7); + + @Override + public void onKeyPress(GridKeyPressEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + grid.addKeyPressHandler(new FooterKeyPressHandler<List<Data>>() { + private final VLabel label = labels.get(8); + + @Override + public void onKeyPress(GridKeyPressEvent<List<Data>> event) { + updateLabel(label, event); + } + }); + + } + + private void updateLabel(VLabel label, + AbstractGridKeyEvent<List<Data>, ?> event) { + String type = event.getNativeEvent().getType(); + Cell active = event.getActiveCell(); + String coords = "(" + active.getRow() + ", " + active.getColumn() + ")"; + String keyCode = "" + event.getNativeKeyCode(); + label.setText(coords + " " + type + " " + keyCode); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java new file mode 100644 index 0000000000..7a9f8a06f5 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java @@ -0,0 +1,378 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window.Location; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HasWidgets; +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.GridColumn; +import com.vaadin.client.ui.grid.Renderer; +import com.vaadin.client.ui.grid.datasources.ListDataSource; +import com.vaadin.client.ui.grid.datasources.ListSorter; +import com.vaadin.client.ui.grid.renderers.ComplexRenderer; +import com.vaadin.client.ui.grid.renderers.DateRenderer; +import com.vaadin.client.ui.grid.renderers.HtmlRenderer; +import com.vaadin.client.ui.grid.renderers.NumberRenderer; +import com.vaadin.client.ui.grid.renderers.TextRenderer; +import com.vaadin.client.ui.grid.renderers.WidgetRenderer; +import com.vaadin.client.ui.grid.sort.Sort; +import com.vaadin.client.ui.grid.sort.SortEvent; +import com.vaadin.client.ui.grid.sort.SortEventHandler; +import com.vaadin.client.ui.grid.sort.SortOrder; +import com.vaadin.shared.ui.Connect; +import com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers; + +@Connect(GridClientColumnRenderers.GridController.class) +public class GridClientColumnRendererConnector extends + AbstractComponentConnector { + + public static enum Renderers { + TEXT_RENDERER, WIDGET_RENDERER, HTML_RENDERER, NUMBER_RENDERER, DATE_RENDERER, CPLX_RENDERER; + } + + /** + * Datasource for simulating network latency + */ + private class DelayedDataSource implements DataSource<String> { + + private DataSource<String> ds; + private int firstRowIndex = -1; + private int numberOfRows; + private DataChangeHandler dataChangeHandler; + private int latency; + + public DelayedDataSource(DataSource<String> ds, int latency) { + this.ds = ds; + this.latency = latency; + } + + @Override + public void ensureAvailability(final int firstRowIndex, + final int numberOfRows) { + new Timer() { + + @Override + public void run() { + DelayedDataSource.this.firstRowIndex = firstRowIndex; + DelayedDataSource.this.numberOfRows = numberOfRows; + dataChangeHandler.dataUpdated(firstRowIndex, numberOfRows); + dataChangeHandler + .dataAvailable(firstRowIndex, numberOfRows); + } + }.schedule(latency); + } + + @Override + public String getRow(int rowIndex) { + if (rowIndex >= firstRowIndex + && rowIndex <= firstRowIndex + numberOfRows) { + return ds.getRow(rowIndex); + } + return null; + } + + @Override + public int getEstimatedSize() { + return ds.getEstimatedSize(); + } + + @Override + public void setDataChangeHandler(DataChangeHandler dataChangeHandler) { + this.dataChangeHandler = dataChangeHandler; + } + + @Override + public RowHandle<String> getHandle(String row) { + // TODO Auto-generated method stub (henrik paul: 17.6.) + return null; + } + } + + @Override + protected void init() { + Grid<String> grid = getWidget(); + grid.setSelectionMode(Grid.SelectionMode.NONE); + + // Generated some column data + List<String> columnData = new ArrayList<String>(); + for (int i = 0; i < 100; i++) { + columnData.add(String.valueOf(i)); + } + + // Provide data as data source + if (Location.getParameter("latency") != null) { + grid.setDataSource(new DelayedDataSource( + new ListDataSource<String>(columnData), Integer + .parseInt(Location.getParameter("latency")))); + } else { + grid.setDataSource(new ListDataSource<String>(columnData)); + } + + // Add a column to display the data in + GridColumn<String, String> c = createColumnWithRenderer(Renderers.TEXT_RENDERER); + grid.addColumn(c); + grid.getHeader().getDefaultRow().getCell(0).setText("Column 1"); + + // Add another column with a custom complex renderer + c = createColumnWithRenderer(Renderers.CPLX_RENDERER); + grid.addColumn(c); + grid.getHeader().getDefaultRow().getCell(1).setText("Column 2"); + + // Add method for testing sort event firing + grid.addSortHandler(new SortEventHandler<String>() { + @Override + public void sort(SortEvent<String> event) { + Element console = Document.get().getElementById( + "testDebugConsole"); + String text = "Client-side sort event received<br>" + + "Columns: " + event.getOrder().size() + ", order: "; + for (SortOrder order : event.getOrder()) { + int colIdx = getWidget().getColumns().indexOf( + order.getColumn()); + String columnHeader = getWidget().getHeader() + .getDefaultRow().getCell(colIdx).getText(); + text += columnHeader + ": " + + order.getDirection().toString(); + } + console.setInnerHTML(text); + } + }); + + // Handle RPC calls + registerRpc(GridClientColumnRendererRpc.class, + new GridClientColumnRendererRpc() { + + @Override + public void addColumn(Renderers renderer) { + + if (renderer == Renderers.NUMBER_RENDERER) { + GridColumn<Number, String> numberColumn = createNumberColumnWithRenderer(renderer); + getWidget().addColumn(numberColumn); + + } else if (renderer == Renderers.DATE_RENDERER) { + GridColumn<Date, String> dateColumn = createDateColumnWithRenderer(renderer); + getWidget().addColumn(dateColumn); + + } else { + GridColumn<String, String> column = createColumnWithRenderer(renderer); + getWidget().addColumn(column); + } + + int idx = getWidget().getColumnCount() - 1; + getWidget() + .getHeader() + .getDefaultRow() + .getCell(idx) + .setText( + "Column " + + String.valueOf(getWidget() + .getColumnCount() + 1)); + } + + @Override + public void detachAttach() { + + // Detach + HasWidgets parent = (HasWidgets) getWidget() + .getParent(); + parent.remove(getWidget()); + + // Re-attach + parent.add(getWidget()); + } + + @Override + public void triggerClientSorting() { + getWidget().sort(Sort.by(getWidget().getColumn(0))); + } + + @Override + @SuppressWarnings("unchecked") + public void triggerClientSortingTest() { + Grid<String> grid = getWidget(); + ListSorter<String> sorter = new ListSorter<String>(grid); + + // Make sorter sort the numbers in natural order + sorter.setComparator( + (GridColumn<String, String>) grid.getColumn(0), + new Comparator<String>() { + @Override + public int compare(String o1, String o2) { + return Integer.parseInt(o1) + - Integer.parseInt(o2); + } + }); + + // Sort along column 0 in ascending order + grid.sort(grid.getColumn(0)); + + // Remove the sorter once we're done + sorter.removeFromGrid(); + } + + @Override + @SuppressWarnings("unchecked") + public void shuffle() { + Grid<String> grid = getWidget(); + ListSorter<String> shuffler = new ListSorter<String>( + grid); + + // Make shuffler return random order + shuffler.setComparator( + (GridColumn<String, String>) grid.getColumn(0), + new Comparator<String>() { + @Override + public int compare(String o1, String o2) { + return com.google.gwt.user.client.Random + .nextInt(3) - 1; + } + }); + + // "Sort" (actually shuffle) along column 0 + grid.sort(grid.getColumn(0)); + + // Remove the shuffler when we're done so that it + // doesn't interfere with further operations + shuffler.removeFromGrid(); + } + }); + } + + /** + * Creates a a renderer for a {@link Renderers} + */ + private Renderer createRenderer(Renderers renderer) { + switch (renderer) { + case TEXT_RENDERER: + return new TextRenderer(); + + case WIDGET_RENDERER: + return new WidgetRenderer<String, Button>() { + + @Override + public Button createWidget() { + final Button button = new Button(""); + button.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + button.setText("Clicked"); + } + }); + return button; + } + + @Override + public void render(FlyweightCell cell, String data, + Button button) { + button.setHTML(data); + } + }; + + case HTML_RENDERER: + return new HtmlRenderer() { + + @Override + public void render(FlyweightCell cell, String htmlString) { + super.render(cell, "<b>" + htmlString + "</b>"); + } + }; + + case NUMBER_RENDERER: + return new NumberRenderer<Long>(); + + case DATE_RENDERER: + return new DateRenderer(); + + case CPLX_RENDERER: + return new ComplexRenderer<String>() { + + @Override + public void render(FlyweightCell cell, String data) { + cell.getElement().setInnerHTML("<span>" + data + "</span>"); + cell.getElement().getStyle().clearBackgroundColor(); + } + + @Override + public void setContentVisible(FlyweightCell cell, + boolean hasData) { + + // Visualize content visible property + cell.getElement().getStyle() + .setBackgroundColor(hasData ? "green" : "red"); + + super.setContentVisible(cell, hasData); + } + }; + + default: + return new TextRenderer(); + } + } + + private GridColumn<String, String> createColumnWithRenderer( + Renderers renderer) { + return new GridColumn<String, String>(createRenderer(renderer)) { + + @Override + public String getValue(String row) { + return row; + } + }; + } + + private GridColumn<Number, String> createNumberColumnWithRenderer( + Renderers renderer) { + return new GridColumn<Number, String>(createRenderer(renderer)) { + + @Override + public Number getValue(String row) { + return Long.parseLong(row); + } + }; + } + + private GridColumn<Date, String> createDateColumnWithRenderer( + Renderers renderer) { + return new GridColumn<Date, String>(createRenderer(renderer)) { + + @Override + public Date getValue(String row) { + return new Date(); + } + }; + } + + @Override + public Grid<String> getWidget() { + return (Grid<String>) super.getWidget(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java new file mode 100644 index 0000000000..90eee9e1c6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers; + +public interface GridClientColumnRendererRpc extends ClientRpc { + + /** + * Adds a new column with a specific renderer to the grid + * + */ + void addColumn(Renderers renderer); + + /** + * Detaches and attaches the client side Grid + */ + void detachAttach(); + + /** + * Used for client-side sorting API test + */ + void triggerClientSorting(); + + /** + * @since + */ + void triggerClientSortingTest(); + + /** + * @since + */ + void shuffle(); +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java new file mode 100644 index 0000000000..d6873ac0a5 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; +import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector; +import com.vaadin.shared.ui.Connect; + +@Connect(com.vaadin.tests.components.grid.IntArrayRenderer.class) +public class IntArrayRendererConnector extends AbstractRendererConnector<int[]> { + + public static class IntArrayRenderer implements Renderer<int[]> { + private static final String JOINER = " :: "; + + @Override + public void render(FlyweightCell cell, int[] data) { + String text = ""; + for (int i : data) { + text += i + JOINER; + } + if (!text.isEmpty()) { + text = text.substring(0, text.length() - JOINER.length()); + } + cell.getElement().setInnerText(text); + } + } + + @Override + public IntArrayRenderer getRenderer() { + return (IntArrayRenderer) super.getRenderer(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java new file mode 100644 index 0000000000..e9c126f232 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java @@ -0,0 +1,308 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.google.gwt.user.client.ui.LayoutPanel; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.Panel; +import com.vaadin.client.ui.SubPartAware; + +/** + * Pure GWT Test Application base for testing features of a single widget; + * provides a menu system and convenience method for adding items to it. + * + * @since + * @author Vaadin Ltd + */ +public abstract class PureGWTTestApplication<T> extends DockLayoutPanel + implements SubPartAware { + + /** + * Class describing a menu item with an associated action + */ + public static class Command { + private final String title; + private final ScheduledCommand command; + + /** + * Creates a Command object, which is used as an action entry in the + * Menu + * + * @param t + * a title string + * @param cmd + * a scheduled command that is executed when this item is + * selected + */ + public Command(String t, ScheduledCommand cmd) { + title = t; + command = cmd; + } + + /** + * Returns the title of this command item + * + * @return a title string + */ + public final String getTitle() { + return title; + } + + /** + * Returns the actual scheduled command of this command item + * + * @return a scheduled command + */ + public final ScheduledCommand getCommand() { + return command; + } + } + + /** + * A menu object, providing a complete system for building a hierarchical + * menu bar system. + */ + public static class Menu { + + private final String title; + private final MenuBar menubar; + private final List<Menu> children; + private final List<Command> items; + + /** + * Create base-level menu, without a title. This is the root menu bar, + * which can be attached to a client application window. All other Menus + * should be added as child menus to this Menu, in order to maintain a + * nice hierarchy. + */ + private Menu() { + title = ""; + menubar = new MenuBar(); + children = new ArrayList<Menu>(); + items = new ArrayList<Command>(); + } + + /** + * Create a sub-menu, with a title. + * + * @param title + */ + public Menu(String title) { + this.title = title; + menubar = new MenuBar(true); + children = new ArrayList<Menu>(); + items = new ArrayList<Command>(); + } + + /** + * Return the GWT {@link MenuBar} object that provides the widget for + * this Menu + * + * @return a menubar object + */ + public MenuBar getMenuBar() { + return menubar; + } + + /** + * Returns the title of this menu entry + * + * @return a title string + */ + public String getTitle() { + return title; + } + + /** + * Adds a child menu entry to this menu. The title for this entry is + * taken from the Menu object argument. + * + * @param m + * another Menu object + */ + public void addChildMenu(Menu m) { + menubar.addItem(m.title, m.menubar); + children.add(m); + } + + /** + * Tests for the existence of a child menu by title at this level of the + * menu hierarchy + * + * @param title + * a title string + * @return true, if this menu has a direct child menu with the specified + * title, otherwise false + */ + public boolean hasChildMenu(String title) { + return getChildMenu(title) != null; + } + + /** + * Gets a reference to a child menu with a certain title, that is a + * direct child of this menu level. + * + * @param title + * a title string + * @return a Menu object with the specified title string, or null, if + * this menu doesn't have a direct child with the specified + * title. + */ + public Menu getChildMenu(String title) { + for (Menu m : children) { + if (m.title.equals(title)) { + return m; + } + } + return null; + } + + /** + * Adds a command item to the menu. When the entry is clicked, the + * command is executed. + * + * @param cmd + * a command object. + */ + public void addCommand(Command cmd) { + menubar.addItem(cmd.title, cmd.command); + items.add(cmd); + } + + /** + * Tests for the existence of a {@link Command} that is the direct child + * of this level of menu. + * + * @param title + * the command's title + * @return true, if this menu level includes a command item with the + * specified title. Otherwise false. + */ + public boolean hasCommand(String title) { + return getCommand(title) != null; + } + + /** + * Gets a reference to a {@link Command} item that is the direct child + * of this level of menu. + * + * @param title + * the command's title + * @return a command, if found in this menu level, otherwise null. + */ + public Command getCommand(String title) { + for (Command c : items) { + if (c.title.equals(title)) { + return c; + } + } + return null; + } + } + + /** + * Base level menu object, provides visible menu bar + */ + private final Menu menu; + private final T testedWidget; + + /** + * This constructor creates the basic menu bar and adds it to the top of the + * parent {@link DockLayoutPanel} + */ + protected PureGWTTestApplication(T widget) { + super(Unit.PX); + Panel menuPanel = new LayoutPanel(); + menu = new Menu(); + menuPanel.add(menu.getMenuBar()); + addNorth(menuPanel, 25); + testedWidget = widget; + } + + /** + * Connect an item to the menu structure + * + * @param cmd + * a scheduled command; see google's docs + * @param menupath + * path to the item + */ + public void addMenuCommand(String title, ScheduledCommand cmd, + String... menupath) { + Menu m = createMenuPath(menupath); + + m.addCommand(new Command(title, cmd)); + } + + /** + * Create a menu path, if one doesn't already exist, and return the last + * menu in the series. + * + * @param path + * a varargs list or array of strings describing a menu path, + * e.g. "File", "Recent", "User Files", which would result in the + * File menu having a submenu called "Recent" which would have a + * submenu called "User Files". + * @return the last Menu object specified by the path + */ + private Menu createMenuPath(String... path) { + Menu m = menu; + + for (String p : path) { + Menu sub = m.getChildMenu(p); + + if (sub == null) { + sub = new Menu(p); + m.addChildMenu(sub); + } + m = sub; + } + + return m; + } + + @Override + public Element getSubPartElement(String subPart) { + if (testedWidget instanceof SubPartAware) { + return ((SubPartAware) testedWidget).getSubPartElement(subPart); + } + return null; + } + + @Override + public String getSubPartName(Element subElement) { + if (testedWidget instanceof SubPartAware) { + return ((SubPartAware) testedWidget).getSubPartName(subElement); + } + return null; + } + + /** + * Gets the tested widget. + * + * @return tested widget + */ + public T getTestedWidget() { + return testedWidget; + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java new file mode 100644 index 0000000000..3880bacae2 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import java.util.Arrays; +import java.util.Collection; + +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.user.client.DOM; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Renderer; +import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector; +import com.vaadin.client.ui.grid.renderers.ComplexRenderer; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.ui.Connect; + +@Connect(com.vaadin.tests.components.grid.RowAwareRenderer.class) +public class RowAwareRendererConnector extends AbstractRendererConnector<Void> { + public interface RowAwareRendererRpc extends ServerRpc { + void clicky(String key); + } + + public class RowAwareRenderer extends ComplexRenderer<Void> { + + @Override + public Collection<String> getConsumedEvents() { + return Arrays.asList(BrowserEvents.CLICK); + } + + @Override + public void init(FlyweightCell cell) { + DivElement div = DivElement.as(DOM.createDiv()); + div.setAttribute("style", + "border: 1px solid red; background: pink;"); + div.setInnerText("Click me!"); + cell.getElement().appendChild(div); + } + + @Override + public void render(FlyweightCell cell, Void data) { + // NOOP + } + + @Override + public boolean onBrowserEvent(Cell cell, NativeEvent event) { + int row = cell.getRow(); + String key = getRowKey(row); + getRpcProxy(RowAwareRendererRpc.class).clicky(key); + cell.getElement().setInnerText("row: " + row + ", key: " + key); + return true; + } + } + + @Override + protected Renderer<Void> createRenderer() { + // cannot use the default createRenderer as RowAwareRenderer needs a + // reference to its connector - it has no "real" no-argument constructor + return new RowAwareRenderer(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java new file mode 100644 index 0000000000..ae2799d228 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import com.vaadin.shared.communication.ClientRpc; + +public interface TestGridClientRpc extends ClientRpc { + void insertRows(int offset, int amount); + + void removeRows(int offset, int amount); + + void insertColumns(int offset, int amount); + + void removeColumns(int offset, int amount); + + void scrollToRow(int index, String destination, int padding); + + void scrollToColumn(int index, String destination, int padding); + + void setFrozenColumns(int frozenColumns); + + void insertHeaders(int index, int amount); + + void removeHeaders(int index, int amount); + + void insertFooters(int index, int amount); + + void removeFooters(int index, int amount); + + void setColumnWidth(int index, int px); + + void calculateColumnWidths(); + + void randomRowHeight(); +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java new file mode 100644 index 0000000000..6dbff5ca66 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java @@ -0,0 +1,138 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import com.google.gwt.user.client.Random; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.tests.widgetset.server.grid.TestGrid; + +/** + * @since + * @author Vaadin Ltd + */ +@Connect(TestGrid.class) +public class TestGridConnector extends AbstractComponentConnector { + @Override + protected void init() { + super.init(); + registerRpc(TestGridClientRpc.class, new TestGridClientRpc() { + @Override + public void insertRows(int offset, int amount) { + getWidget().insertRows(offset, amount); + } + + @Override + public void removeRows(int offset, int amount) { + getWidget().removeRows(offset, amount); + } + + @Override + public void removeColumns(int offset, int amount) { + getWidget().removeColumns(offset, amount); + } + + @Override + public void insertColumns(int offset, int amount) { + getWidget().insertColumns(offset, amount); + } + + @Override + public void scrollToRow(int index, String destination, int padding) { + getWidget().scrollToRow(index, getDestination(destination), + padding); + } + + @Override + public void scrollToColumn(int index, String destination, + int padding) { + getWidget().scrollToColumn(index, getDestination(destination), + padding); + } + + private ScrollDestination getDestination(String destination) { + final ScrollDestination d; + if (destination.equals("start")) { + d = ScrollDestination.START; + } else if (destination.equals("middle")) { + d = ScrollDestination.MIDDLE; + } else if (destination.equals("end")) { + d = ScrollDestination.END; + } else { + d = ScrollDestination.ANY; + } + return d; + } + + @Override + public void setFrozenColumns(int frozenColumns) { + getWidget().getColumnConfiguration().setFrozenColumnCount( + frozenColumns); + } + + @Override + public void insertHeaders(int index, int amount) { + getWidget().getHeader().insertRows(index, amount); + } + + @Override + public void removeHeaders(int index, int amount) { + getWidget().getHeader().removeRows(index, amount); + } + + @Override + public void insertFooters(int index, int amount) { + getWidget().getFooter().insertRows(index, amount); + } + + @Override + public void removeFooters(int index, int amount) { + getWidget().getFooter().removeRows(index, amount); + } + + @Override + public void setColumnWidth(int index, int px) { + getWidget().getColumnConfiguration().setColumnWidth(index, px); + } + + @Override + public void calculateColumnWidths() { + getWidget().calculateColumnWidths(); + } + + @Override + public void randomRowHeight() { + getWidget().getHeader().setDefaultRowHeight( + Random.nextInt(20) + 20); + getWidget().getBody().setDefaultRowHeight( + Random.nextInt(20) + 20); + getWidget().getFooter().setDefaultRowHeight( + Random.nextInt(20) + 20); + } + }); + } + + @Override + public VTestGrid getWidget() { + return (VTestGrid) super.getWidget(); + } + + @Override + public TestGridState getState() { + return (TestGridState) super.getState(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java new file mode 100644 index 0000000000..ecbc59552b --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.client.grid; + +import com.vaadin.shared.AbstractComponentState; + +/** + * @since + * @author Vaadin Ltd + */ +public class TestGridState extends AbstractComponentState { + public static final String DEFAULT_HEIGHT = "400.0px"; + + /* TODO: this should be "100%" before setting final. */ + public static final String DEFAULT_WIDTH = "800.0px"; +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java new file mode 100644 index 0000000000..fbce00fc11 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java @@ -0,0 +1,249 @@ +package com.vaadin.tests.widgetset.client.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.user.client.Window.Location; +import com.google.gwt.user.client.ui.Composite; +import com.vaadin.client.ui.grid.ColumnConfiguration; +import com.vaadin.client.ui.grid.Escalator; +import com.vaadin.client.ui.grid.EscalatorUpdater; +import com.vaadin.client.ui.grid.FlyweightCell; +import com.vaadin.client.ui.grid.Row; +import com.vaadin.client.ui.grid.RowContainer; +import com.vaadin.shared.ui.grid.ScrollDestination; + +public class VTestGrid extends Composite { + + private static abstract class TestEscalatorUpdater implements + EscalatorUpdater { + + @Override + public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) { + } + + @Override + public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) { + } + + @Override + public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) { + } + + @Override + public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) { + } + } + + private static class Data { + private int columnCounter = 0; + private int rowCounter = 0; + private final List<Integer> columns = new ArrayList<Integer>(); + private final List<Integer> rows = new ArrayList<Integer>(); + + @SuppressWarnings("boxing") + public void insertRows(final int offset, final int amount) { + final List<Integer> newRows = new ArrayList<Integer>(); + for (int i = 0; i < amount; i++) { + newRows.add(rowCounter++); + } + rows.addAll(offset, newRows); + } + + @SuppressWarnings("boxing") + public void insertColumns(final int offset, final int amount) { + final List<Integer> newColumns = new ArrayList<Integer>(); + for (int i = 0; i < amount; i++) { + newColumns.add(columnCounter++); + } + columns.addAll(offset, newColumns); + } + + public EscalatorUpdater createHeaderUpdater() { + return new TestEscalatorUpdater() { + @Override + public void update(final Row row, + final Iterable<FlyweightCell> cellsToUpdate) { + for (final FlyweightCell cell : cellsToUpdate) { + if (cell.getColumn() % 3 == 0) { + cell.setColSpan(2); + } + + final Integer columnName = columns + .get(cell.getColumn()); + cell.getElement().setInnerText("Header " + columnName); + } + } + }; + } + + public EscalatorUpdater createFooterUpdater() { + return new TestEscalatorUpdater() { + @Override + public void update(final Row row, + final Iterable<FlyweightCell> cellsToUpdate) { + for (final FlyweightCell cell : cellsToUpdate) { + if (cell.getColumn() % 3 == 1) { + cell.setColSpan(2); + } + + final Integer columnName = columns + .get(cell.getColumn()); + cell.getElement().setInnerText("Footer " + columnName); + } + } + }; + } + + public EscalatorUpdater createBodyUpdater() { + return new TestEscalatorUpdater() { + private int i = 0; + + public void renderCell(final FlyweightCell cell) { + final Integer columnName = columns.get(cell.getColumn()); + final Integer rowName = rows.get(cell.getRow()); + String cellInfo = columnName + "," + rowName; + if (shouldRenderPretty()) { + cellInfo += " (" + i + ")"; + } + + if (cell.getColumn() > 0) { + cell.getElement().setInnerText("Cell: " + cellInfo); + } else { + cell.getElement().setInnerText( + "Row " + cell.getRow() + ": " + cellInfo); + } + + if (cell.getColumn() % 3 == cell.getRow() % 3) { + cell.setColSpan(3); + } + + if (shouldRenderPretty()) { + final double c = i * .1; + final int r = (int) ((Math.cos(c) + 1) * 128); + final int g = (int) ((Math.cos(c / Math.PI) + 1) * 128); + final int b = (int) ((Math.cos(c / (Math.PI * 2)) + 1) * 128); + cell.getElement() + .getStyle() + .setBackgroundColor( + "rgb(" + r + "," + g + "," + b + ")"); + if ((r * .8 + g * 1.3 + b * .9) / 3 < 127) { + cell.getElement().getStyle().setColor("white"); + } else { + cell.getElement().getStyle().clearColor(); + } + } + + i++; + } + + private boolean shouldRenderPretty() { + return Location.getQueryString().contains("pretty"); + } + + @Override + public void update(final Row row, + final Iterable<FlyweightCell> cellsToUpdate) { + for (final FlyweightCell cell : cellsToUpdate) { + renderCell(cell); + } + } + }; + } + + public void removeRows(final int offset, final int amount) { + for (int i = 0; i < amount; i++) { + rows.remove(offset); + } + } + + public void removeColumns(final int offset, final int amount) { + for (int i = 0; i < amount; i++) { + columns.remove(offset); + } + } + } + + private final Escalator escalator = new Escalator(); + private final Data data = new Data(); + + public VTestGrid() { + initWidget(escalator); + final RowContainer header = escalator.getHeader(); + header.setEscalatorUpdater(data.createHeaderUpdater()); + header.insertRows(0, 1); + + final RowContainer footer = escalator.getFooter(); + footer.setEscalatorUpdater(data.createFooterUpdater()); + footer.insertRows(0, 1); + + escalator.getBody().setEscalatorUpdater(data.createBodyUpdater()); + + insertRows(0, 100); + insertColumns(0, 10); + + setWidth(TestGridState.DEFAULT_WIDTH); + setHeight(TestGridState.DEFAULT_HEIGHT); + + } + + public void insertRows(final int offset, final int number) { + data.insertRows(offset, number); + escalator.getBody().insertRows(offset, number); + } + + public void insertColumns(final int offset, final int number) { + data.insertColumns(offset, number); + escalator.getColumnConfiguration().insertColumns(offset, number); + } + + public ColumnConfiguration getColumnConfiguration() { + return escalator.getColumnConfiguration(); + } + + public void scrollToRow(final int index, + final ScrollDestination destination, final int padding) { + escalator.scrollToRow(index, destination, padding); + } + + public void scrollToColumn(final int index, + final ScrollDestination destination, final int padding) { + escalator.scrollToColumn(index, destination, padding); + } + + public void removeRows(final int offset, final int amount) { + data.removeRows(offset, amount); + escalator.getBody().removeRows(offset, amount); + } + + public void removeColumns(final int offset, final int amount) { + data.removeColumns(offset, amount); + escalator.getColumnConfiguration().removeColumns(offset, amount); + } + + @Override + public void setWidth(String width) { + escalator.setWidth(width); + } + + @Override + public void setHeight(String height) { + escalator.setHeight(height); + } + + public RowContainer getHeader() { + return escalator.getHeader(); + } + + public RowContainer getBody() { + return escalator.getBody(); + } + + public RowContainer getFooter() { + return escalator.getFooter(); + } + + public void calculateColumnWidths() { + escalator.calculateColumnWidths(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java new file mode 100644 index 0000000000..db931888bc --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.server.grid; + +import java.util.Arrays; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers; +import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererRpc; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.NativeButton; +import com.vaadin.ui.NativeSelect; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; + +@Widgetset(TestingWidgetSet.NAME) +public class GridClientColumnRenderers extends UI { + + /** + * Controls the grid on the client side + */ + public static class GridController extends AbstractComponent { + + private GridClientColumnRendererRpc rpc() { + return getRpcProxy(GridClientColumnRendererRpc.class); + } + + /** + * Adds a new column with a renderer to the grid. + */ + public void addColumn(Renderers renderer) { + rpc().addColumn(renderer); + } + + /** + * Tests detaching and attaching grid + */ + public void detachAttach() { + rpc().detachAttach(); + } + + /** + * @since + */ + public void triggerClientSorting() { + rpc().triggerClientSorting(); + } + + /** + * @since + */ + public void triggerClientSortingTest() { + rpc().triggerClientSortingTest(); + } + + /** + * @since + */ + public void shuffle() { + rpc().shuffle(); + } + } + + @Override + protected void init(VaadinRequest request) { + final GridController controller = new GridController(); + final CssLayout controls = new CssLayout(); + final VerticalLayout content = new VerticalLayout(); + + content.addComponent(controller); + content.addComponent(controls); + setContent(content); + + final NativeSelect select = new NativeSelect( + "Add Column with Renderer", Arrays.asList(Renderers.values())); + select.setValue(Renderers.TEXT_RENDERER); + select.setNullSelectionAllowed(false); + controls.addComponent(select); + + NativeButton addColumnBtn = new NativeButton("Add"); + addColumnBtn.addClickListener(new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + Renderers renderer = (Renderers) select.getValue(); + controller.addColumn(renderer); + } + }); + controls.addComponent(addColumnBtn); + + NativeButton detachAttachBtn = new NativeButton("DetachAttach"); + detachAttachBtn.addClickListener(new ClickListener() { + + @Override + public void buttonClick(ClickEvent event) { + controller.detachAttach(); + } + }); + controls.addComponent(detachAttachBtn); + + NativeButton shuffleButton = new NativeButton("Shuffle"); + shuffleButton.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + controller.shuffle(); + } + }); + controls.addComponent(shuffleButton); + + NativeButton sortButton = new NativeButton("Trigger sorting event"); + sortButton.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + controller.triggerClientSorting(); + } + }); + controls.addComponent(sortButton); + + NativeButton testSortingButton = new NativeButton("Test sorting"); + testSortingButton.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + controller.triggerClientSortingTest(); + } + }); + controls.addComponent(testSortingButton); + + Label console = new Label(); + console.setContentMode(ContentMode.HTML); + console.setId("testDebugConsole"); + content.addComponent(console); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java new file mode 100644 index 0000000000..0dbb60359d --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.widgetset.server.grid; + +import com.vaadin.tests.widgetset.client.grid.TestGridClientRpc; +import com.vaadin.tests.widgetset.client.grid.TestGridState; +import com.vaadin.ui.AbstractComponent; + +/** + * @since + * @author Vaadin Ltd + */ +public class TestGrid extends AbstractComponent { + public TestGrid() { + setWidth(TestGridState.DEFAULT_WIDTH); + setHeight(TestGridState.DEFAULT_HEIGHT); + } + + @Override + protected TestGridState getState() { + return (TestGridState) super.getState(); + } + + public void insertRows(int offset, int amount) { + rpc().insertRows(offset, amount); + } + + public void removeRows(int offset, int amount) { + rpc().removeRows(offset, amount); + } + + public void insertColumns(int offset, int amount) { + rpc().insertColumns(offset, amount); + } + + public void removeColumns(int offset, int amount) { + rpc().removeColumns(offset, amount); + } + + private TestGridClientRpc rpc() { + return getRpcProxy(TestGridClientRpc.class); + } + + public void scrollToRow(int index, String destination, int padding) { + rpc().scrollToRow(index, destination, padding); + } + + public void scrollToColumn(int index, String destination, int padding) { + rpc().scrollToColumn(index, destination, padding); + } + + public void setFrozenColumns(int frozenColumns) { + rpc().setFrozenColumns(frozenColumns); + } + + public void insertHeaders(int index, int amount) { + rpc().insertHeaders(index, amount); + } + + public void removeHeaders(int index, int amount) { + rpc().removeHeaders(index, amount); + } + + public void insertFooters(int index, int amount) { + rpc().insertFooters(index, amount); + } + + public void removeFooters(int index, int amount) { + rpc().removeFooters(index, amount); + } + + public void setColumnWidth(int index, int px) { + rpc().setColumnWidth(index, px); + } + + public void calculateColumnWidths() { + rpc().calculateColumnWidths(); + } + + public void randomizeDefaultRowHeight() { + rpc().randomRowHeight(); + } +} |