From: Teemu Suo-Anttila Date: Wed, 31 Aug 2016 13:03:20 +0000 (+0300) Subject: Add simple data change handling for Selects X-Git-Tag: 8.0.0.alpha1~32 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6d0204cafe8c95acd72728b3d1be616d87a31db1;p=vaadin-framework.git Add simple data change handling for Selects Change-Id: I16f9577ea4091fb4febe167d76e141b5945f53ab --- diff --git a/client/src/main/java/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/main/java/com/vaadin/client/data/AbstractRemoteDataSource.java index 95e013ed28..71568cac7a 100644 --- a/client/src/main/java/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/main/java/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -28,8 +28,8 @@ 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.Range; import com.vaadin.shared.Registration; -import com.vaadin.shared.ui.grid.Range; /** * Base implementation for data sources that fetch data from a remote system. diff --git a/client/src/main/java/com/vaadin/client/data/CacheStrategy.java b/client/src/main/java/com/vaadin/client/data/CacheStrategy.java index 9410223ab2..acfadcbcf7 100644 --- a/client/src/main/java/com/vaadin/client/data/CacheStrategy.java +++ b/client/src/main/java/com/vaadin/client/data/CacheStrategy.java @@ -16,7 +16,7 @@ package com.vaadin.client.data; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; /** * Determines what data an {@link AbstractRemoteDataSource} should fetch and diff --git a/client/src/main/java/com/vaadin/client/data/DataSource.java b/client/src/main/java/com/vaadin/client/data/DataSource.java index 3457b7b9fc..fdec4b9a38 100644 --- a/client/src/main/java/com/vaadin/client/data/DataSource.java +++ b/client/src/main/java/com/vaadin/client/data/DataSource.java @@ -16,6 +16,9 @@ package com.vaadin.client.data; +import java.util.function.Consumer; + +import com.vaadin.shared.Range; import com.vaadin.shared.Registration; /** @@ -104,7 +107,7 @@ public interface DataSource { * override of an existing method, we're defining a new method for that * instead. * - * @param rowHandle + * @param obj * the reference object with which to compare * @return {@code true} if this object is the same as the obj argument; * {@code false} otherwise. @@ -182,10 +185,27 @@ public interface DataSource { * * @param dataChangeHandler * the data change handler + * + * @return registration for removing the handler */ public Registration addDataChangeHandler( DataChangeHandler dataChangeHandler); + /** + * Sets a simple data change handler for a widget without lazy loading. + * Refresh method should reset all the data in the widget. + * + * @param refreshMethod + * a method to refresh all data in the widget + * + * @return registration for removing the handler + */ + public default Registration addDataChangeHandler( + Consumer refreshMethod) { + return addDataChangeHandler( + new SimpleDataChangeHandler(this, refreshMethod)); + } + /** * Gets a {@link RowHandle} of a row object in the cache. * diff --git a/client/src/main/java/com/vaadin/client/data/SimpleDataChangeHandler.java b/client/src/main/java/com/vaadin/client/data/SimpleDataChangeHandler.java new file mode 100644 index 0000000000..46fb019f3c --- /dev/null +++ b/client/src/main/java/com/vaadin/client/data/SimpleDataChangeHandler.java @@ -0,0 +1,103 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF 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.function.Consumer; + +import com.google.gwt.core.client.Scheduler; +import com.vaadin.shared.Range; + +/** + * Helper class for creating a {@link DataChangeHandler} for a Widget that does + * not support lazy loading. + * + * @author Vaadin Ltd + * @since + */ +public class SimpleDataChangeHandler implements DataChangeHandler { + + /** + * Class to request the data source to get the full data set. + */ + private static class DelayedResetScheduler { + + private final DataSource dataSource; + private boolean scheduled = false; + + public DelayedResetScheduler(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void schedule() { + if (scheduled) { + return; + } + Scheduler.get().scheduleFinally(() -> { + dataSource.ensureAvailability(0, dataSource.size()); + scheduled = false; + }); + scheduled = true; + } + + public int getExpectedSize() { + return dataSource.size(); + } + + public boolean isScheduled() { + return scheduled; + } + } + + private final DelayedResetScheduler scheduler; + private final Consumer refreshMethod; + + SimpleDataChangeHandler(DataSource dataSource, + Consumer refreshMethod) { + scheduler = new DelayedResetScheduler(dataSource); + this.refreshMethod = refreshMethod; + } + + @Override + public void dataUpdated(int firstRowIndex, int numberOfRows) { + scheduler.schedule(); + } + + @Override + public void dataRemoved(int firstRowIndex, int numberOfRows) { + scheduler.schedule(); + } + + @Override + public void dataAdded(int firstRowIndex, int numberOfRows) { + scheduler.schedule(); + } + + @Override + public void dataAvailable(int firstRowIndex, int numberOfRows) { + if (!scheduler.isScheduled() && firstRowIndex == 0 + && numberOfRows == scheduler.getExpectedSize()) { + // All data should now be available. + refreshMethod.accept(Range.withLength(firstRowIndex, numberOfRows)); + } else { + scheduler.schedule(); + } + } + + @Override + public void resetDataAndSize(int newSize) { + scheduler.schedule(); + } +} diff --git a/client/src/main/java/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java b/client/src/main/java/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java index ce4be72b68..a36b591fd0 100644 --- a/client/src/main/java/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java +++ b/client/src/main/java/com/vaadin/client/widget/escalator/RowVisibilityChangeEvent.java @@ -17,7 +17,7 @@ package com.vaadin.client.widget.escalator; import com.google.gwt.event.shared.GwtEvent; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; /** * Event fired when the range of visible rows changes e.g. because of scrolling. diff --git a/client/src/main/java/com/vaadin/client/widget/grid/DataAvailableEvent.java b/client/src/main/java/com/vaadin/client/widget/grid/DataAvailableEvent.java index f4ecf11677..8441a16bc5 100644 --- a/client/src/main/java/com/vaadin/client/widget/grid/DataAvailableEvent.java +++ b/client/src/main/java/com/vaadin/client/widget/grid/DataAvailableEvent.java @@ -16,7 +16,7 @@ package com.vaadin.client.widget.grid; import com.google.gwt.event.shared.GwtEvent; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; /** * Event object describing a change of row availability in DataSource of a Grid. diff --git a/client/src/main/java/com/vaadin/client/widgets/Escalator.java b/client/src/main/java/com/vaadin/client/widgets/Escalator.java index a5e95a26a5..35978b7cb0 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Escalator.java +++ b/client/src/main/java/com/vaadin/client/widgets/Escalator.java @@ -89,8 +89,8 @@ import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent; import com.vaadin.client.widget.grid.events.ScrollEvent; import com.vaadin.client.widget.grid.events.ScrollHandler; import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle; +import com.vaadin.shared.Range; 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; diff --git a/client/src/main/java/com/vaadin/client/widgets/Grid.java b/client/src/main/java/com/vaadin/client/widgets/Grid.java index 990c85dd1c..85c6d48942 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Grid.java +++ b/client/src/main/java/com/vaadin/client/widgets/Grid.java @@ -174,13 +174,13 @@ import com.vaadin.client.widgets.Escalator.SubPartArguments; import com.vaadin.client.widgets.Grid.Editor.State; import com.vaadin.client.widgets.Grid.StaticSection.StaticCell; import com.vaadin.client.widgets.Grid.StaticSection.StaticRow; +import com.vaadin.shared.Range; import com.vaadin.shared.Registration; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridConstants.Section; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.HeightMode; -import com.vaadin.shared.ui.grid.Range; import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.util.SharedUtil; diff --git a/client/src/test/java/com/vaadin/client/ui/grid/PartitioningTest.java b/client/src/test/java/com/vaadin/client/ui/grid/PartitioningTest.java index ac4d39006a..8be729e7c4 100644 --- a/client/src/test/java/com/vaadin/client/ui/grid/PartitioningTest.java +++ b/client/src/test/java/com/vaadin/client/ui/grid/PartitioningTest.java @@ -21,7 +21,7 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; @SuppressWarnings("static-method") public class PartitioningTest { diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java index bdaf0f1f88..5103abb285 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/MultiSelectionModelConnector.java @@ -29,8 +29,8 @@ import com.vaadin.client.ServerConnector; import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.data.DataSource; import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.shared.Range; import com.vaadin.shared.ui.Connect; -import com.vaadin.shared.ui.grid.Range; import com.vaadin.v7.client.renderers.ComplexRenderer; import com.vaadin.v7.client.renderers.Renderer; import com.vaadin.v7.client.widget.grid.DataAvailableEvent; diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/RpcDataSourceConnector.java b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/RpcDataSourceConnector.java index 439305d718..363d3c047d 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/RpcDataSourceConnector.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/connectors/RpcDataSourceConnector.java @@ -23,10 +23,10 @@ import java.util.List; import com.vaadin.client.ServerConnector; import com.vaadin.client.data.AbstractRemoteDataSource; import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.shared.Range; import com.vaadin.shared.data.DataProviderRpc; import com.vaadin.shared.data.DataRequestRpc; import com.vaadin.shared.ui.Connect; -import com.vaadin.shared.ui.grid.Range; import com.vaadin.v7.shared.ui.grid.GridState; import elemental.json.Json; diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java index d5c0001594..ad2b6c65a6 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/escalator/RowVisibilityChangeEvent.java @@ -17,7 +17,7 @@ package com.vaadin.v7.client.widget.escalator; import com.google.gwt.event.shared.GwtEvent; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; /** * Event fired when the range of visible rows changes e.g. because of scrolling. diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java index 37b171894d..9f51c73047 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widget/grid/DataAvailableEvent.java @@ -16,7 +16,7 @@ package com.vaadin.v7.client.widget.grid; import com.google.gwt.event.shared.GwtEvent; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; /** * Event object describing a change of row availability in DataSource of a Grid. diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java index f8f4848508..0804a0ad5e 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Escalator.java @@ -65,7 +65,7 @@ import com.vaadin.client.DeferredWorker; import com.vaadin.client.Profiler; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.SubPartAware; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; import com.vaadin.shared.util.SharedUtil; import com.vaadin.v7.client.widget.escalator.Cell; import com.vaadin.v7.client.widget.escalator.ColumnConfiguration; diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java index 9069c95e41..e79237702b 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/widgets/Grid.java @@ -90,9 +90,9 @@ import com.vaadin.client.ui.dd.DragAndDropHandler.DragAndDropCallback; import com.vaadin.client.ui.dd.DragHandle; import com.vaadin.client.ui.dd.DragHandle.DragHandleCallback; import com.vaadin.client.widgets.Overlay; +import com.vaadin.shared.Range; import com.vaadin.shared.Registration; import com.vaadin.shared.data.sort.SortDirection; -import com.vaadin.shared.ui.grid.Range; import com.vaadin.shared.util.SharedUtil; import com.vaadin.v7.client.renderers.ComplexRenderer; import com.vaadin.v7.client.renderers.Renderer; diff --git a/compatibility-server/src/main/java/com/vaadin/v7/server/communication/data/RpcDataProviderExtension.java b/compatibility-server/src/main/java/com/vaadin/v7/server/communication/data/RpcDataProviderExtension.java index ce960fa5fd..7fdcd4eff4 100644 --- a/compatibility-server/src/main/java/com/vaadin/v7/server/communication/data/RpcDataProviderExtension.java +++ b/compatibility-server/src/main/java/com/vaadin/v7/server/communication/data/RpcDataProviderExtension.java @@ -29,9 +29,9 @@ import java.util.Set; import com.vaadin.server.AbstractExtension; import com.vaadin.server.ClientConnector; import com.vaadin.server.KeyMapper; +import com.vaadin.shared.Range; import com.vaadin.shared.data.DataProviderRpc; import com.vaadin.shared.data.DataRequestRpc; -import com.vaadin.shared.ui.grid.Range; import com.vaadin.v7.data.Container; import com.vaadin.v7.data.Container.Indexed; import com.vaadin.v7.data.Container.Indexed.ItemAddEvent; diff --git a/server/src/main/java/com/vaadin/server/data/DataCommunicator.java b/server/src/main/java/com/vaadin/server/data/DataCommunicator.java index 7bf9707741..8c92ad50e3 100644 --- a/server/src/main/java/com/vaadin/server/data/DataCommunicator.java +++ b/server/src/main/java/com/vaadin/server/data/DataCommunicator.java @@ -31,10 +31,10 @@ import java.util.stream.Stream; import com.vaadin.server.AbstractExtension; import com.vaadin.server.KeyMapper; +import com.vaadin.shared.Range; import com.vaadin.shared.data.DataCommunicatorClientRpc; import com.vaadin.shared.data.DataCommunicatorConstants; import com.vaadin.shared.data.DataRequestRpc; -import com.vaadin.shared.ui.grid.Range; import elemental.json.Json; import elemental.json.JsonArray; diff --git a/shared/src/main/java/com/vaadin/shared/Range.java b/shared/src/main/java/com/vaadin/shared/Range.java new file mode 100644 index 0000000000..98d9ba54e2 --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/Range.java @@ -0,0 +1,439 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS 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; + +import java.io.Serializable; + +/** + * An immutable representation of a range, marked by start and end points. + *

+ * 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. + *

+ * The range is considered {@link #isEmpty() empty} if the start is the same as + * the end. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public final class Range implements Serializable { + private final int start; + private final int end; + + /** + * Creates a range object representing a single integer. + * + * @param integer + * the number to represent as a range + * @return the range represented by integer + */ + public static Range withOnly(final int integer) { + return new Range(integer, integer + 1); + } + + /** + * Creates a range between two integers. + *

+ * The range start is inclusive and the end is exclusive. + * 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 [start..end[ + * @throws IllegalArgumentException + * if start > end + */ + 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 start, with + * length 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: [start..end[. + * + * @param start + * the start integer, inclusive + * @param end + * the end integer, exclusive + * @throws IllegalArgumentException + * if start > end + */ + 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 inclusive start point of this range. + * + * @return the start point of this range + */ + public int getStart() { + return start; + } + + /** + * Returns the exclusive 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 true 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 true if this and other 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 true iff integer 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 true iff other completely wraps this + * range + */ + public boolean isSubsetOf(final Range other) { + if (isEmpty() && other.isEmpty()) { + return true; + } + + return other.getStart() <= getStart() && getEnd() <= other.getEnd(); + } + + /** + * Overlay this range with another one, and partition the ranges according + * to how they position relative to each other. + *

+ * The three partitions are returned as a three-element Range array: + *

    + *
  • Elements in this range that occur before elements in + * other. + *
  • Elements that are shared between the two ranges. + *
  • Elements in this range that occur after elements in + * other. + *
+ * + * @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 offset + */ + 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 true iff this range starts before the + * other + */ + 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 true iff this range ends before the + * other + */ + 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 true iff this range ends after the + * other + */ + 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 true iff this range starts after the + * other + */ + public boolean startsAfter(final Range other) { + return getStart() >= other.getEnd(); + } + + /** + * Split the range into two at a certain integer. + *

+ * Example: [5..10[.splitAt(7) == [5..7[, [7..10[ + * + * @param integer + * the integer at which to split the range into two + * @return an array of two ranges, with [start..integer[ in the + * first element, and [integer..end[ in the second + * element. + *

+ * If {@code integer} is less than {@code start}, [empty, + * {@code this} ] is returned. if integer 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. + *

+ * Calling this method is equivalent to calling + * {@link #splitAt(int) splitAt}({@link #getStart()}+length); + *

+ * Example: + * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ + * + * @param length + * the length at which to split this range into two + * @return an array of two ranges, having the length-first + * elements of this range, and the second range having the rest. If + * length ≤ 0, the first element will be empty, and + * the second element will be this range. If length + * ≥ {@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 start > end + */ + 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. + *

+ * This is basically an optimized way of calculating + * {@link #partitionWith(Range)}[1] without the overhead of + * defining the parts that do not overlap. + *

+ * If the two ranges do not intersect, an empty range is returned. There are + * no guarantees about the position of that range. + * + * @param bounds + * the bounds that the returned range should be limited to + * @return a bounded range + */ + public Range restrictTo(Range bounds) { + boolean startWithin = bounds.contains(getStart()); + boolean endWithin = bounds.contains(getEnd()); + boolean boundsWithin = getStart() < bounds.getStart() + && getEnd() >= bounds.getEnd(); + + if (startWithin) { + if (endWithin) { + return this; + } else { + return Range.between(getStart(), bounds.getEnd()); + } + } else { + if (endWithin) { + return Range.between(bounds.getStart(), getEnd()); + } else if (boundsWithin) { + return bounds; + } else { + return Range.withLength(getStart(), 0); + } + } + } +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/Range.java b/shared/src/main/java/com/vaadin/shared/ui/grid/Range.java deleted file mode 100644 index 93805e4c0b..0000000000 --- a/shared/src/main/java/com/vaadin/shared/ui/grid/Range.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS 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. - *

- * 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. - *

- * The range is considered {@link #isEmpty() empty} if the start is the same as - * the end. - * - * @since 7.4 - * @author Vaadin Ltd - */ -public final class Range implements Serializable { - private final int start; - private final int end; - - /** - * Creates a range object representing a single integer. - * - * @param integer - * the number to represent as a range - * @return the range represented by integer - */ - public static Range withOnly(final int integer) { - return new Range(integer, integer + 1); - } - - /** - * Creates a range between two integers. - *

- * The range start is inclusive and the end is exclusive. - * 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 [start..end[ - * @throws IllegalArgumentException - * if start > end - */ - 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 start, with - * length 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: [start..end[. - * - * @param start - * the start integer, inclusive - * @param end - * the end integer, exclusive - * @throws IllegalArgumentException - * if start > end - */ - 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 inclusive start point of this range. - * - * @return the start point of this range - */ - public int getStart() { - return start; - } - - /** - * Returns the exclusive 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 true 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 true if this and other 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 true iff integer 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 true iff other completely wraps this - * range - */ - public boolean isSubsetOf(final Range other) { - if (isEmpty() && other.isEmpty()) { - return true; - } - - return other.getStart() <= getStart() && getEnd() <= other.getEnd(); - } - - /** - * Overlay this range with another one, and partition the ranges according - * to how they position relative to each other. - *

- * The three partitions are returned as a three-element Range array: - *

    - *
  • Elements in this range that occur before elements in - * other. - *
  • Elements that are shared between the two ranges. - *
  • Elements in this range that occur after elements in - * other. - *
- * - * @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 offset - */ - 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 true iff this range starts before the - * other - */ - 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 true iff this range ends before the - * other - */ - 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 true iff this range ends after the - * other - */ - 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 true iff this range starts after the - * other - */ - public boolean startsAfter(final Range other) { - return getStart() >= other.getEnd(); - } - - /** - * Split the range into two at a certain integer. - *

- * Example: [5..10[.splitAt(7) == [5..7[, [7..10[ - * - * @param integer - * the integer at which to split the range into two - * @return an array of two ranges, with [start..integer[ in the - * first element, and [integer..end[ in the second - * element. - *

- * If {@code integer} is less than {@code start}, [empty, - * {@code this} ] is returned. if integer 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. - *

- * Calling this method is equivalent to calling - * {@link #splitAt(int) splitAt}({@link #getStart()}+length); - *

- * Example: - * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ - * - * @param length - * the length at which to split this range into two - * @return an array of two ranges, having the length-first - * elements of this range, and the second range having the rest. If - * length ≤ 0, the first element will be empty, and - * the second element will be this range. If length - * ≥ {@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 start > end - */ - 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. - *

- * This is basically an optimized way of calculating - * {@link #partitionWith(Range)}[1] without the overhead of - * defining the parts that do not overlap. - *

- * If the two ranges do not intersect, an empty range is returned. There are - * no guarantees about the position of that range. - * - * @param bounds - * the bounds that the returned range should be limited to - * @return a bounded range - */ - public Range restrictTo(Range bounds) { - boolean startWithin = bounds.contains(getStart()); - boolean endWithin = bounds.contains(getEnd()); - boolean boundsWithin = getStart() < bounds.getStart() - && getEnd() >= bounds.getEnd(); - - if (startWithin) { - if (endWithin) { - return this; - } else { - return Range.between(getStart(), bounds.getEnd()); - } - } else { - if (endWithin) { - return Range.between(bounds.getStart(), getEnd()); - } else if (boundsWithin) { - return bounds; - } else { - return Range.withLength(getStart(), 0); - } - } - } -} diff --git a/shared/src/test/java/com/vaadin/shared/ui/grid/RangeTest.java b/shared/src/test/java/com/vaadin/shared/ui/grid/RangeTest.java index bde581022b..921edf145e 100644 --- a/shared/src/test/java/com/vaadin/shared/ui/grid/RangeTest.java +++ b/shared/src/test/java/com/vaadin/shared/ui/grid/RangeTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; +import com.vaadin.shared.Range; + @SuppressWarnings("static-method") public class RangeTest { diff --git a/uitest/src/main/java/com/vaadin/tests/data/DummyData.java b/uitest/src/main/java/com/vaadin/tests/data/DummyData.java new file mode 100644 index 0000000000..60b3af7681 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/data/DummyData.java @@ -0,0 +1,117 @@ +package com.vaadin.tests.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.data.DataCommunicator; +import com.vaadin.server.data.ListDataSource; +import com.vaadin.server.data.Query; +import com.vaadin.shared.data.DataCommunicatorConstants; +import com.vaadin.shared.data.selection.SelectionModel; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.ui.AbstractListing; +import com.vaadin.ui.Button; +import com.vaadin.ui.HorizontalLayout; + +@Widgetset(TestingWidgetSet.NAME) +public class DummyData extends AbstractTestUIWithLog { + + /** + * DataSource that keeps track on how often the data is requested. + */ + private class LoggingDataSource extends ListDataSource { + private int count = 0; + + private LoggingDataSource(Collection collection) { + super(collection); + } + + @Override + public Stream apply(Query query) { + log("Backend request #" + (count++)); + return super.apply(query); + } + } + + /** + * Simplified server only selection model. Selection state passed in data, + * shown as bold text. + */ + private static class DummySelectionModel implements SelectionModel { + private String selected; + private DataCommunicator communicator; + + @Override + public Set getSelectedItems() { + if (selected != null) { + return Collections.singleton(selected); + } + return Collections.emptySet(); + } + + @Override + public void select(String item) { + if (selected != null) { + communicator.refresh(selected); + } + selected = item; + if (selected != null) { + communicator.refresh(selected); + } + } + + @Override + public void deselect(String item) { + if (item == selected) { + select(null); + } + } + + private void setCommunicator(DataCommunicator dataComm) { + communicator = dataComm; + } + } + + public static class DummyComponent + extends AbstractListing { + + private DummyComponent() { + setSelectionModel(new DummySelectionModel()); + addDataGenerator((str, json) -> { + json.put(DataCommunicatorConstants.DATA, str); + if (isSelected(str)) { + json.put(DataCommunicatorConstants.SELECTED, true); + } + }); + getSelectionModel().setCommunicator(getDataCommunicator()); + } + } + + @Override + protected void setup(VaadinRequest request) { + DummyComponent dummy = new DummyComponent(); + List items = new ArrayList<>(); + for (int i = 0; i < 300; ++i) { + items.add("Foo " + i); + } + dummy.setDataSource(new LoggingDataSource(items)); + dummy.select("Foo 200"); + + HorizontalLayout controls = new HorizontalLayout(); + addComponent(controls); + controls.addComponent(new Button("Select Foo 20", e -> { + dummy.select("Foo " + 20); + })); + controls.addComponent(new Button("Reset data source", e -> { + dummy.setDataSource(new LoggingDataSource(items)); + })); + addComponent(dummy); + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/widgetset/client/data/DummyComponentConnector.java b/uitest/src/main/java/com/vaadin/tests/widgetset/client/data/DummyComponentConnector.java new file mode 100644 index 0000000000..061b52abf5 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/widgetset/client/data/DummyComponentConnector.java @@ -0,0 +1,43 @@ +package com.vaadin.tests.widgetset.client.data; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.vaadin.client.connectors.AbstractListingConnector; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.VLabel; +import com.vaadin.shared.data.DataCommunicatorConstants; +import com.vaadin.shared.ui.Connect; +import com.vaadin.tests.data.DummyData.DummyComponent; + +import elemental.json.JsonObject; + +@Connect(DummyComponent.class) +public class DummyComponentConnector extends AbstractListingConnector { + + @Override + public FlowPanel getWidget() { + return (FlowPanel) super.getWidget(); + } + + @Override + public void setDataSource(DataSource dataSource) { + super.setDataSource(dataSource); + + dataSource.addDataChangeHandler(range -> { + assert range.getStart() == 0 && range.getEnd() == dataSource + .size() : "Widget only supports full updates."; + getWidget().clear(); + for (int i = range.getStart(); i < range.getEnd(); ++i) { + VLabel label = new VLabel(); + getWidget().add(label); + JsonObject row = dataSource.getRow(i); + String text = row.getString(DataCommunicatorConstants.DATA); + if (row.hasKey(DataCommunicatorConstants.SELECTED) + && row.getBoolean(DataCommunicatorConstants.SELECTED)) { + text = "" + text + ""; + label.addStyleName("selected"); + } + label.setHTML(text); + } + }); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java index ed15f13f34..1b00d14050 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsClientTest.java @@ -30,7 +30,7 @@ import org.junit.Test; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; import com.vaadin.testbench.By; import com.vaadin.testbench.ElementQuery; import com.vaadin.testbench.TestBenchElement; diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index f389b4b3f3..93931d50ec 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -33,7 +33,7 @@ import org.openqa.selenium.Keys; import org.openqa.selenium.WebElement; import com.vaadin.client.WidgetUtil; -import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.Range; import com.vaadin.testbench.TestBenchElement; import com.vaadin.testbench.elements.NotificationElement; import com.vaadin.testbench.parallel.BrowserUtil; diff --git a/uitest/src/test/java/com/vaadin/tests/data/DummyDataTest.java b/uitest/src/test/java/com/vaadin/tests/data/DummyDataTest.java new file mode 100644 index 0000000000..48001ef27f --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/data/DummyDataTest.java @@ -0,0 +1,60 @@ +package com.vaadin.tests.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class DummyDataTest extends SingleBrowserTest { + + @Before + public void setUp() { + setDebug(true); + openTestURL(); + } + + @Test + public void testCorrectRowSelectedOnInit() { + List selected = findElements(By.className("selected")); + assertTrue("Only one should be selected", 1 == selected.size()); + assertEquals("Wrong item selected", "Foo 200", + selected.get(0).getText()); + } + + @Test + public void testServerSelectionUpdatesSelected() { + $(ButtonElement.class).first().click(); + List selected = findElements(By.className("selected")); + assertTrue("Only one should be selected", 1 == selected.size()); + assertEquals("Wrong item selected", "Foo 20", + selected.get(0).getText()); + } + + @Test + public void testDataUpdateDoesNotCauseBackEndRequest() { + assertEquals("Unexpected backend requests", "2. Backend request #1", + getLogRow(0)); + assertEquals("Unexpected backend requests", "1. Backend request #0", + getLogRow(1)); + // Select a row on the server-side, triggers an update + $(ButtonElement.class).first().click(); + assertEquals("No requests should have happened", + "2. Backend request #1", getLogRow(0)); + } + + @Test + public void testDataSourceChangeOnlyOneRequest() { + // Change to a new logging data source + $(ButtonElement.class).get(1).click(); + assertEquals("DataSource change should only cause 1 request", + "3. Backend request #0", getLogRow(0)); + } +}