diff options
author | Artur Signell <artur@vaadin.com> | 2015-09-04 15:05:27 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2015-09-04 15:05:27 +0300 |
commit | f46be1b2792755cb7d9b111068dd6cf202398c4a (patch) | |
tree | 74f7098faee688cc1f1ca1b6ffe9e912338bbcaf /client | |
parent | e603ea3cf91e5dd0893b766f27d400947527fba9 (diff) | |
parent | ebceef4d44bcd61605fa92fcf7be8d3678537599 (diff) | |
download | vaadin-framework-f46be1b2792755cb7d9b111068dd6cf202398c4a.tar.gz vaadin-framework-f46be1b2792755cb7d9b111068dd6cf202398c4a.zip |
Merge remote-tracking branch 'origin/master' into reconnect-dialog
Change-Id: Ie622160a83116c83b255a26bec297f73f3223ac7
Diffstat (limited to 'client')
48 files changed, 2859 insertions, 1452 deletions
diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index 70275218cf..6e20908274 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -62,7 +62,6 @@ import com.vaadin.client.ui.VContextMenu; import com.vaadin.client.ui.VNotification; import com.vaadin.client.ui.VOverlay; import com.vaadin.client.ui.ui.UIConnector; -import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.VaadinUriResolver; import com.vaadin.shared.Version; import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; @@ -1318,20 +1317,19 @@ public class ApplicationConnection implements HasHandlers { * before the component is updated so the value is correct if called from * updatedFromUIDL. * - * @param paintable + * @param connector * The connector to register event listeners for * @param eventIdentifier * The identifier for the event * @return true if at least one listener has been registered on server side * for the event identified by eventIdentifier. * @deprecated As of 7.0. Use - * {@link AbstractComponentState#hasEventListener(String)} - * instead + * {@link AbstractConnector#hasEventListener(String)} instead */ @Deprecated - public boolean hasEventListeners(ComponentConnector paintable, + public boolean hasEventListeners(ComponentConnector connector, String eventIdentifier) { - return paintable.hasEventListener(eventIdentifier); + return connector.hasEventListener(eventIdentifier); } /** diff --git a/client/src/com/vaadin/client/BrowserInfo.java b/client/src/com/vaadin/client/BrowserInfo.java index 8b274623c1..8dcefddcf5 100644 --- a/client/src/com/vaadin/client/BrowserInfo.java +++ b/client/src/com/vaadin/client/BrowserInfo.java @@ -30,6 +30,7 @@ public class BrowserInfo { private static final String BROWSER_OPERA = "op"; private static final String BROWSER_IE = "ie"; + private static final String BROWSER_EDGE = "edge"; private static final String BROWSER_FIREFOX = "ff"; private static final String BROWSER_SAFARI = "sa"; @@ -171,6 +172,13 @@ public class BrowserInfo { minorVersionClass = majorVersionClass + browserDetails.getBrowserMinorVersion(); browserEngineClass = ENGINE_TRIDENT; + } else if (browserDetails.isEdge()) { + browserIdentifier = BROWSER_EDGE; + majorVersionClass = browserIdentifier + + getBrowserMajorVersion(); + minorVersionClass = majorVersionClass + + browserDetails.getBrowserMinorVersion(); + browserEngineClass = ""; } else if (browserDetails.isOpera()) { browserIdentifier = BROWSER_OPERA; majorVersionClass = browserIdentifier @@ -225,6 +233,10 @@ public class BrowserInfo { return browserDetails.isIE(); } + public boolean isEdge() { + return browserDetails.isEdge(); + } + public boolean isFirefox() { return browserDetails.isFirefox(); } @@ -245,6 +257,10 @@ public class BrowserInfo { return isIE() && getBrowserMajorVersion() == 10; } + public boolean isIE11() { + return isIE() && getBrowserMajorVersion() == 11; + } + public boolean isChrome() { return browserDetails.isChrome(); } diff --git a/client/src/com/vaadin/client/ComputedStyle.java b/client/src/com/vaadin/client/ComputedStyle.java index 61cb3c2eb6..66404eeeed 100644 --- a/client/src/com/vaadin/client/ComputedStyle.java +++ b/client/src/com/vaadin/client/ComputedStyle.java @@ -155,10 +155,10 @@ public class ComputedStyle { * the property to retrieve * @return the double value of the property */ - public final int getDoubleProperty(String name) { + public final double getDoubleProperty(String name) { Profiler.enter("ComputedStyle.getDoubleProperty"); String value = getProperty(name); - int result = parseDoubleNative(value); + double result = parseDoubleNative(value); Profiler.leave("ComputedStyle.getDoubleProperty"); return result; } @@ -275,9 +275,87 @@ public class ComputedStyle { * @return the value from the string before any non-numeric characters or * NaN if the value cannot be parsed as a number */ - private static native int parseDoubleNative(final String value) + private static native double parseDoubleNative(final String value) /*-{ return parseFloat(value); }-*/; + /** + * Returns the sum of the top and bottom border width + * + * @since 7.5.3 + * @return the sum of the top and bottom border + */ + public double getBorderHeight() { + double borderHeight = getDoubleProperty("borderTopWidth"); + borderHeight += getDoubleProperty("borderBottomWidth"); + + return borderHeight; + } + + /** + * Returns the sum of the left and right border width + * + * @since 7.5.3 + * @return the sum of the left and right border + */ + public double getBorderWidth() { + double borderWidth = getDoubleProperty("borderLeftWidth"); + borderWidth += getDoubleProperty("borderRightWidth"); + + return borderWidth; + } + + /** + * Returns the sum of the top and bottom padding + * + * @since 7.5.3 + * @return the sum of the top and bottom padding + */ + public double getPaddingHeight() { + double paddingHeight = getDoubleProperty("paddingTop"); + paddingHeight += getDoubleProperty("paddingBottom"); + + return paddingHeight; + } + + /** + * Returns the sum of the top and bottom padding + * + * @since 7.5.3 + * @return the sum of the left and right padding + */ + public double getPaddingWidth() { + double paddingWidth = getDoubleProperty("paddingLeft"); + paddingWidth += getDoubleProperty("paddingRight"); + + return paddingWidth; + } + + /** + * Returns the sum of the top and bottom margin + * + * @since 7.6 + * @return the sum of the top and bottom margin + */ + public double getMarginHeight() { + double marginHeight = getDoubleProperty("marginTop"); + marginHeight += getDoubleProperty("marginBottom"); + + return marginHeight; + } + + /** + * Returns the sum of the top and bottom margin + * + * @since 7.6 + * @return the sum of the left and right margin + */ + public double getMarginWidth() { + double marginWidth = getDoubleProperty("marginLeft"); + marginWidth += getDoubleProperty("marginRight"); + + return marginWidth; + } + } diff --git a/client/src/com/vaadin/client/EventHelper.java b/client/src/com/vaadin/client/EventHelper.java index f251215d41..1ee252af0f 100644 --- a/client/src/com/vaadin/client/EventHelper.java +++ b/client/src/com/vaadin/client/EventHelper.java @@ -51,7 +51,6 @@ import com.google.gwt.user.client.ui.Widget; * * * </pre> - * */ public class EventHelper { @@ -69,7 +68,7 @@ public class EventHelper { */ public static <T extends ComponentConnector & FocusHandler> HandlerRegistration updateFocusHandler( T connector, HandlerRegistration handlerRegistration) { - return updateHandler(connector, FOCUS, handlerRegistration, + return updateHandler(connector, connector, FOCUS, handlerRegistration, FocusEvent.getType(), connector.getWidget()); } @@ -89,7 +88,7 @@ public class EventHelper { */ public static <T extends ComponentConnector & FocusHandler> HandlerRegistration updateFocusHandler( T connector, HandlerRegistration handlerRegistration, Widget widget) { - return updateHandler(connector, FOCUS, handlerRegistration, + return updateHandler(connector, connector, FOCUS, handlerRegistration, FocusEvent.getType(), widget); } @@ -107,7 +106,7 @@ public class EventHelper { */ public static <T extends ComponentConnector & BlurHandler> HandlerRegistration updateBlurHandler( T connector, HandlerRegistration handlerRegistration) { - return updateHandler(connector, BLUR, handlerRegistration, + return updateHandler(connector, connector, BLUR, handlerRegistration, BlurEvent.getType(), connector.getWidget()); } @@ -128,23 +127,21 @@ public class EventHelper { */ public static <T extends ComponentConnector & BlurHandler> HandlerRegistration updateBlurHandler( T connector, HandlerRegistration handlerRegistration, Widget widget) { - return updateHandler(connector, BLUR, handlerRegistration, + return updateHandler(connector, connector, BLUR, handlerRegistration, BlurEvent.getType(), widget); } - private static <H extends EventHandler> HandlerRegistration updateHandler( - ComponentConnector connector, String eventIdentifier, + public static <H extends EventHandler> HandlerRegistration updateHandler( + ComponentConnector connector, H handler, String eventIdentifier, HandlerRegistration handlerRegistration, Type<H> type, Widget widget) { if (connector.hasEventListener(eventIdentifier)) { if (handlerRegistration == null) { - handlerRegistration = widget.addDomHandler((H) connector, type); + handlerRegistration = widget.addDomHandler(handler, type); } } else if (handlerRegistration != null) { handlerRegistration.removeHandler(); handlerRegistration = null; } return handlerRegistration; - } - } diff --git a/client/src/com/vaadin/client/ResourceLoader.java b/client/src/com/vaadin/client/ResourceLoader.java index 9e9ce5ac49..559768d09c 100644 --- a/client/src/com/vaadin/client/ResourceLoader.java +++ b/client/src/com/vaadin/client/ResourceLoader.java @@ -283,8 +283,7 @@ public class ResourceLoader { * @since 7.2.4 */ public static boolean supportsInOrderScriptExecution() { - return BrowserInfo.get().isIE() - && BrowserInfo.get().getBrowserMajorVersion() >= 11; + return BrowserInfo.get().isIE11() || BrowserInfo.get().isEdge(); } /** @@ -486,10 +485,11 @@ public class ResourceLoader { addOnloadHandler(linkElement, new ResourceLoadListener() { @Override public void onLoad(ResourceLoadEvent event) { - // Chrome && IE fires load for errors, must check + // Chrome, IE, Edge all fire load for errors, must check // stylesheet data if (BrowserInfo.get().isChrome() - || BrowserInfo.get().isIE()) { + || BrowserInfo.get().isIE() + || BrowserInfo.get().isEdge()) { int styleSheetLength = getStyleSheetLength(url); // Error if there's an empty stylesheet if (styleSheetLength == 0) { diff --git a/client/src/com/vaadin/client/SuperDevMode.java b/client/src/com/vaadin/client/SuperDevMode.java index c72cd73939..f664244715 100644 --- a/client/src/com/vaadin/client/SuperDevMode.java +++ b/client/src/com/vaadin/client/SuperDevMode.java @@ -189,7 +189,11 @@ public class SuperDevMode { if (serverUrl == null || "".equals(serverUrl)) { serverUrl = "http://localhost:9876/"; } else { - serverUrl = "http://" + serverUrl + "/"; + if (serverUrl.contains(":")) { + serverUrl = "http://" + serverUrl + "/"; + } else { + serverUrl = "http://" + serverUrl + ":9876/"; + } } if (hasSession(SKIP_RECOMPILE)) { diff --git a/client/src/com/vaadin/client/VTooltip.java b/client/src/com/vaadin/client/VTooltip.java index 4e59040298..f3d65cd20a 100644 --- a/client/src/com/vaadin/client/VTooltip.java +++ b/client/src/com/vaadin/client/VTooltip.java @@ -89,6 +89,9 @@ public class VTooltip extends VOverlay { LiveValue.ASSERTIVE); Roles.getTooltipRole().setAriaRelevantProperty(getElement(), RelevantValue.ADDITIONS); + + // Tooltip needs to be on top of other VOverlay elements. + setZIndex(VOverlay.Z_INDEX + 1); } /** diff --git a/client/src/com/vaadin/client/connectors/AbstractSelectionModelConnector.java b/client/src/com/vaadin/client/connectors/AbstractSelectionModelConnector.java new file mode 100644 index 0000000000..8ca2292bc5 --- /dev/null +++ b/client/src/com/vaadin/client/connectors/AbstractSelectionModelConnector.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.connectors; + +import java.util.Collection; + +import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.widget.grid.selection.SelectionModel; +import com.vaadin.client.widgets.Grid; +import com.vaadin.shared.ui.grid.GridState; + +import elemental.json.JsonObject; + +/** + * Base class for all selection model connectors. + * + * @since + * @author Vaadin Ltd + */ +public abstract class AbstractSelectionModelConnector<T extends SelectionModel<JsonObject>> + extends AbstractExtensionConnector { + + @Override + public GridConnector getParent() { + return (GridConnector) super.getParent(); + } + + protected Grid<JsonObject> getGrid() { + return getParent().getWidget(); + } + + protected RowHandle<JsonObject> getRowHandle(JsonObject row) { + return getGrid().getDataSource().getHandle(row); + } + + protected String getRowKey(JsonObject row) { + return row != null ? getParent().getRowKey(row) : null; + } + + protected abstract T createSelectionModel(); + + public abstract static class AbstractSelectionModel implements + SelectionModel<JsonObject> { + + @Override + public boolean isSelected(JsonObject row) { + return row.hasKey(GridState.JSONKEY_SELECTED); + } + + @Override + public void setGrid(Grid<JsonObject> grid) { + // NO-OP + } + + @Override + public void reset() { + // Should not need any actions. + } + + @Override + public Collection<JsonObject> getSelectedRows() { + throw new UnsupportedOperationException( + "This client-side selection model " + + getClass().getSimpleName() + + " does not know selected rows."); + } + } +} diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index ef52a429e7..1070a46287 100644 --- a/client/src/com/vaadin/client/connectors/GridConnector.java +++ b/client/src/com/vaadin/client/connectors/GridConnector.java @@ -19,33 +19,36 @@ package com.vaadin.client.connectors; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorHierarchyChangeEvent; import com.vaadin.client.DeferredWorker; import com.vaadin.client.MouseEventDetailsBuilder; -import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.TooltipInfo; import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler; import com.vaadin.client.connectors.RpcDataSourceConnector.DetailsListener; import com.vaadin.client.connectors.RpcDataSourceConnector.RpcDataSource; -import com.vaadin.client.data.DataSource.RowHandle; -import com.vaadin.client.renderers.Renderer; import com.vaadin.client.ui.AbstractFieldConnector; import com.vaadin.client.ui.AbstractHasComponentsConnector; +import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; import com.vaadin.client.ui.SimpleManagedLayout; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; @@ -59,17 +62,12 @@ import com.vaadin.client.widget.grid.events.ColumnReorderEvent; import com.vaadin.client.widget.grid.events.ColumnReorderHandler; import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent; import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler; +import com.vaadin.client.widget.grid.events.EditorCloseEvent; +import com.vaadin.client.widget.grid.events.EditorEventHandler; +import com.vaadin.client.widget.grid.events.EditorMoveEvent; +import com.vaadin.client.widget.grid.events.EditorOpenEvent; import com.vaadin.client.widget.grid.events.GridClickEvent; import com.vaadin.client.widget.grid.events.GridDoubleClickEvent; -import com.vaadin.client.widget.grid.events.SelectAllEvent; -import com.vaadin.client.widget.grid.events.SelectAllHandler; -import com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel; -import com.vaadin.client.widget.grid.selection.SelectionEvent; -import com.vaadin.client.widget.grid.selection.SelectionHandler; -import com.vaadin.client.widget.grid.selection.SelectionModel; -import com.vaadin.client.widget.grid.selection.SelectionModelMulti; -import com.vaadin.client.widget.grid.selection.SelectionModelNone; -import com.vaadin.client.widget.grid.selection.SelectionModelSingle; import com.vaadin.client.widget.grid.sort.SortEvent; import com.vaadin.client.widget.grid.sort.SortHandler; import com.vaadin.client.widget.grid.sort.SortOrder; @@ -79,10 +77,8 @@ import com.vaadin.client.widgets.Grid.FooterCell; import com.vaadin.client.widgets.Grid.FooterRow; import com.vaadin.client.widgets.Grid.HeaderCell; import com.vaadin.client.widgets.Grid.HeaderRow; -import com.vaadin.shared.Connector; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.Connect; -import com.vaadin.shared.ui.grid.DetailsConnectorChange; import com.vaadin.shared.ui.grid.EditorClientRpc; import com.vaadin.shared.ui.grid.EditorServerRpc; import com.vaadin.shared.ui.grid.GridClientRpc; @@ -90,7 +86,6 @@ import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; -import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode; import com.vaadin.shared.ui.grid.GridStaticSectionState; import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; @@ -114,8 +109,8 @@ import elemental.json.JsonValue; public class GridConnector extends AbstractHasComponentsConnector implements SimpleManagedLayout, DeferredWorker { - private static final class CustomCellStyleGenerator implements - CellStyleGenerator<JsonObject> { + private static final class CustomStyleGenerator implements + CellStyleGenerator<JsonObject>, RowStyleGenerator<JsonObject> { @Override public String getStyle(CellReference<JsonObject> cellReference) { JsonObject row = cellReference.getRow(); @@ -141,10 +136,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements } } - } - - private static final class CustomRowStyleGenerator implements - RowStyleGenerator<JsonObject> { @Override public String getStyle(RowReference<JsonObject> rowReference) { JsonObject row = rowReference.getRow(); @@ -154,7 +145,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements return null; } } - } /** @@ -169,6 +159,8 @@ public class GridConnector extends AbstractHasComponentsConnector implements private AbstractFieldConnector editorConnector; + private HandlerRegistration errorStateHandler; + public CustomGridColumn(String id, AbstractRendererConnector<Object> rendererConnector) { super(rendererConnector.getRenderer()); @@ -205,8 +197,54 @@ public class GridConnector extends AbstractHasComponentsConnector implements return editorConnector; } - private void setEditorConnector(AbstractFieldConnector editorConnector) { + private void setEditorConnector( + final AbstractFieldConnector editorConnector) { this.editorConnector = editorConnector; + + if (errorStateHandler != null) { + errorStateHandler.removeHandler(); + errorStateHandler = null; + } + + // Avoid nesting too deep + if (editorConnector == null) { + return; + } + + errorStateHandler = editorConnector.addStateChangeHandler( + "errorMessage", new StateChangeHandler() { + + @Override + public void onStateChanged( + StateChangeEvent stateChangeEvent) { + + String error = editorConnector.getState().errorMessage; + + if (error == null) { + columnToErrorMessage + .remove(CustomGridColumn.this); + } else { + // The error message is formatted as HTML; + // therefore, we use this hack to make the + // string human-readable. + Element e = DOM.createElement("div"); + e.setInnerHTML(editorConnector.getState().errorMessage); + error = getHeaderCaption() + ": " + + e.getInnerText(); + + columnToErrorMessage.put(CustomGridColumn.this, + error); + } + + // Editor should not be touched while there's a + // request pending. + if (editorHandler.currentRequest == null) { + getWidget().getEditor().setEditorError( + getColumnErrors(), + columnToErrorMessage.keySet()); + } + } + }); } } @@ -281,7 +319,12 @@ public class GridConnector extends AbstractHasComponentsConnector implements if (column instanceof CustomGridColumn) { AbstractFieldConnector c = ((CustomGridColumn) column) .getEditorConnector(); - return c != null ? c.getWidget() : null; + + if (c == null) { + return null; + } + + return c.getWidget(); } else { throw new IllegalStateException("Unexpected column type: " + column.getClass().getName()); @@ -420,255 +463,105 @@ public class GridConnector extends AbstractHasComponentsConnector implements } }; - private static class CustomDetailsGenerator implements DetailsGenerator { + private class CustomDetailsGenerator implements DetailsGenerator { - private final Map<Integer, ComponentConnector> indexToDetailsMap = new HashMap<Integer, ComponentConnector>(); + private final Map<String, ComponentConnector> idToDetailsMap = new HashMap<String, ComponentConnector>(); + private final Map<String, Integer> idToRowIndex = new HashMap<String, Integer>(); @Override - @SuppressWarnings("boxing") public Widget getDetails(int rowIndex) { - ComponentConnector componentConnector = indexToDetailsMap - .get(rowIndex); - if (componentConnector != null) { - return componentConnector.getWidget(); - } else { - return null; - } - } - - public void setDetailsConnectorChanges( - Set<DetailsConnectorChange> changes) { - /* - * To avoid overwriting connectors while moving them about, we'll - * take all the affected connectors, first all remove those that are - * removed or moved, then we add back those that are moved or added. - */ + JsonObject row = getWidget().getDataSource().getRow(rowIndex); - /* Remove moved/removed connectors from bookkeeping */ - for (DetailsConnectorChange change : changes) { - Integer oldIndex = change.getOldIndex(); - Connector removedConnector = indexToDetailsMap.remove(oldIndex); - - Connector connector = change.getConnector(); - assert removedConnector == null || connector == null - || removedConnector.equals(connector) : "Index " - + oldIndex + " points to " + removedConnector - + " while " + connector + " was expected"; + if (!row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE) + || row.getString(GridState.JSONKEY_DETAILS_VISIBLE) + .isEmpty()) { + return null; } - /* Add moved/added connectors to bookkeeping */ - for (DetailsConnectorChange change : changes) { - Integer newIndex = change.getNewIndex(); - ComponentConnector connector = (ComponentConnector) change - .getConnector(); - - if (connector != null) { - assert newIndex != null : "An existing connector has a missing new index."; - - ComponentConnector prevConnector = indexToDetailsMap.put( - newIndex, connector); + String id = row.getString(GridState.JSONKEY_DETAILS_VISIBLE); + ComponentConnector componentConnector = idToDetailsMap.get(id); + idToRowIndex.put(id, rowIndex); - assert prevConnector == null : "Connector collision at index " - + newIndex - + " between old " - + prevConnector - + " and new " + connector; - } - } + return componentConnector.getWidget(); } - } - - @SuppressWarnings("boxing") - private static class DetailsConnectorFetcher implements DeferredWorker { - private static final int FETCH_TIMEOUT_MS = 5000; - - public interface Listener { - void fetchHasBeenScheduled(int id); - - void fetchHasReturned(int id); - } - - /** A flag making sure that we don't call scheduleFinally many times. */ - private boolean fetcherHasBeenCalled = false; - - /** A rolling counter for unique values. */ - private int detailsFetchCounter = 0; - - /** A collection that tracks the amount of requests currently underway. */ - private Set<Integer> pendingFetches = new HashSet<Integer>(5); - - private final ScheduledCommand lazyDetailsFetcher = new ScheduledCommand() { - @Override - public void execute() { - int currentFetchId = detailsFetchCounter++; - pendingFetches.add(currentFetchId); - rpc.sendDetailsComponents(currentFetchId); - fetcherHasBeenCalled = false; - - if (listener != null) { - listener.fetchHasBeenScheduled(currentFetchId); + public void updateConnectorHierarchy(List<ServerConnector> children) { + Set<String> connectorIds = new HashSet<String>(); + for (ServerConnector child : children) { + if (child instanceof ComponentConnector) { + connectorIds.add(child.getConnectorId()); + idToDetailsMap.put(child.getConnectorId(), + (ComponentConnector) child); } - - assert assertRequestDoesNotTimeout(currentFetchId); } - }; - - private DetailsConnectorFetcher.Listener listener = null; - - private final GridServerRpc rpc; - public DetailsConnectorFetcher(GridServerRpc rpc) { - assert rpc != null : "RPC was null"; - this.rpc = rpc; - } - - public void schedule() { - if (!fetcherHasBeenCalled) { - Scheduler.get().scheduleFinally(lazyDetailsFetcher); - fetcherHasBeenCalled = true; - } - } - - public void responseReceived(int fetchId) { - - if (fetchId < 0) { - /* Ignore negative fetchIds (they're pushed, not fetched) */ - return; + Set<String> removedDetails = new HashSet<String>(); + for (Entry<String, ComponentConnector> entry : idToDetailsMap + .entrySet()) { + ComponentConnector connector = entry.getValue(); + String id = connector.getConnectorId(); + if (!connectorIds.contains(id)) { + removedDetails.add(entry.getKey()); + if (idToRowIndex.containsKey(id)) { + getWidget().setDetailsVisible(idToRowIndex.get(id), + false); + } + } } - boolean success = pendingFetches.remove(fetchId); - assert success : "Received a response with an unidentified fetch id"; - - if (listener != null) { - listener.fetchHasReturned(fetchId); + for (String id : removedDetails) { + idToDetailsMap.remove(id); + idToRowIndex.remove(id); } } - - @Override - public boolean isWorkPending() { - return fetcherHasBeenCalled || !pendingFetches.isEmpty(); - } - - private boolean assertRequestDoesNotTimeout(final int fetchId) { - /* - * This method will not be compiled without asserts enabled. This - * only makes sure that any request does not time out. - * - * TODO Should this be an explicit check? Is it worth the overhead? - */ - new Timer() { - @Override - public void run() { - assert !pendingFetches.contains(fetchId) : "Fetch id " - + fetchId + " timed out."; - } - }.schedule(FETCH_TIMEOUT_MS); - return true; - } - - public void setListener(DetailsConnectorFetcher.Listener listener) { - // if more are needed, feel free to convert this into a collection. - this.listener = listener; - } } /** - * The functionality that makes sure that the scroll position is still kept - * up-to-date even if more details are being fetched lazily. + * Class for handling scrolling issues with open details. + * + * @since 7.5.2 */ - private class LazyDetailsScrollAdjuster implements DeferredWorker { - - private static final int SCROLL_TO_END_ID = -2; - private static final int NO_SCROLL_SCHEDULED = -1; - - private class ScrollStopChecker implements DeferredWorker { - private final ScheduledCommand checkCommand = new ScheduledCommand() { - @Override - public void execute() { - isScheduled = false; - if (queuedFetches.isEmpty()) { - currentRow = NO_SCROLL_SCHEDULED; - destination = null; - } - } - }; - - private boolean isScheduled = false; - - public void schedule() { - if (isScheduled) { - return; - } - Scheduler.get().scheduleDeferred(checkCommand); - isScheduled = true; - } + private class LazyDetailsScroller implements DeferredWorker { - @Override - public boolean isWorkPending() { - return isScheduled; - } - } + /* Timer value tested to work in our test cluster with slow IE8s. */ + private static final int DISABLE_LAZY_SCROLL_TIMEOUT = 1500; - private DetailsConnectorFetcher.Listener fetcherListener = new DetailsConnectorFetcher.Listener() { + /* + * Cancels details opening scroll after timeout. Avoids any unexpected + * scrolls via details opening. + */ + private Timer disableScroller = new Timer() { @Override - @SuppressWarnings("boxing") - public void fetchHasBeenScheduled(int id) { - if (currentRow != NO_SCROLL_SCHEDULED) { - queuedFetches.add(id); - } - } - - @Override - @SuppressWarnings("boxing") - public void fetchHasReturned(int id) { - if (currentRow == NO_SCROLL_SCHEDULED - || queuedFetches.isEmpty()) { - return; - } - - queuedFetches.remove(id); - if (currentRow == SCROLL_TO_END_ID) { - getWidget().scrollToEnd(); - } else { - getWidget().scrollToRow(currentRow, destination); - } - - /* - * Schedule a deferred call whether we should stop adjusting for - * scrolling. - * - * This is done deferredly just because we can't be absolutely - * certain whether this most recent scrolling won't cascade into - * further lazy details loading (perhaps deferredly). - */ - scrollStopChecker.schedule(); + public void run() { + targetRow = -1; } }; - private int currentRow = NO_SCROLL_SCHEDULED; - private final Set<Integer> queuedFetches = new HashSet<Integer>(); - private final ScrollStopChecker scrollStopChecker = new ScrollStopChecker(); - private ScrollDestination destination; - - public LazyDetailsScrollAdjuster() { - detailsConnectorFetcher.setListener(fetcherListener); - } + private Integer targetRow = -1; + private ScrollDestination destination = null; - public void adjustForEnd() { - currentRow = SCROLL_TO_END_ID; + public void scrollToRow(Integer row, ScrollDestination dest) { + targetRow = row; + destination = dest; + disableScroller.schedule(DISABLE_LAZY_SCROLL_TIMEOUT); } - public void adjustFor(int row, ScrollDestination destination) { - currentRow = row; - this.destination = destination; + /** + * Inform LazyDetailsScroller that a details row has opened on a row. + * + * @param rowIndex + * index of row with details now open + */ + public void detailsOpened(int rowIndex) { + if (targetRow == rowIndex) { + getWidget().scrollToRow(targetRow, destination); + disableScroller.run(); + } } @Override public boolean isWorkPending() { - return currentRow != NO_SCROLL_SCHEDULED - || !queuedFetches.isEmpty() - || scrollStopChecker.isWorkPending(); + return disableScroller.isRunning(); } } @@ -677,20 +570,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements */ private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>(); - private AbstractRowHandleSelectionModel<JsonObject> selectionModel; - private Set<String> selectedKeys = new LinkedHashSet<String>(); private List<String> columnOrder = new ArrayList<String>(); /** - * {@link #selectionUpdatedFromState} is set to true when - * {@link #updateSelectionFromState()} makes changes to selection. This flag - * tells the {@code internalSelectionChangeHandler} to not send same data - * straight back to server. Said listener sets it back to false when - * handling that event. - */ - private boolean selectionUpdatedFromState; - - /** * {@link #columnsUpdatedFromState} is set to true when * {@link #updateColumnOrderFromState(List)} is updating the column order * for the widget. This flag tells the {@link #columnReorderHandler} to not @@ -701,68 +583,57 @@ public class GridConnector extends AbstractHasComponentsConnector implements private RpcDataSource dataSource; - private SelectionHandler<JsonObject> internalSelectionChangeHandler = new SelectionHandler<JsonObject>() { - @Override - public void onSelect(SelectionEvent<JsonObject> event) { - if (event.isBatchedSelection()) { - return; - } - if (!selectionUpdatedFromState) { - for (JsonObject row : event.getRemoved()) { - selectedKeys.remove(dataSource.getRowKey(row)); - } - - for (JsonObject row : event.getAdded()) { - selectedKeys.add(dataSource.getRowKey(row)); - } - - getRpcProxy(GridServerRpc.class).select( - new ArrayList<String>(selectedKeys)); - } else { - selectionUpdatedFromState = false; - } - } - }; + /* Used to track Grid editor columns with validation errors */ + private final Map<Column<?, JsonObject>, String> columnToErrorMessage = new HashMap<Column<?, JsonObject>, String>(); private ItemClickHandler itemClickHandler = new ItemClickHandler(); private String lastKnownTheme = null; private final CustomDetailsGenerator customDetailsGenerator = new CustomDetailsGenerator(); - - private final DetailsConnectorFetcher detailsConnectorFetcher = new DetailsConnectorFetcher( - getRpcProxy(GridServerRpc.class)); + private final CustomStyleGenerator styleGenerator = new CustomStyleGenerator(); private final DetailsListener detailsListener = new DetailsListener() { @Override public void reapplyDetailsVisibility(final int rowIndex, final JsonObject row) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - if (hasDetailsOpen(row)) { - getWidget().setDetailsVisible(rowIndex, true); - detailsConnectorFetcher.schedule(); - } else { + if (hasDetailsOpen(row)) { + // Command for opening details row. + ScheduledCommand openDetails = new ScheduledCommand() { + @Override + public void execute() { + // Re-apply to force redraw. getWidget().setDetailsVisible(rowIndex, false); + getWidget().setDetailsVisible(rowIndex, true); + lazyDetailsScroller.detailsOpened(rowIndex); } + }; + + if (initialChange) { + Scheduler.get().scheduleDeferred(openDetails); + } else { + Scheduler.get().scheduleFinally(openDetails); } - }); + } else { + getWidget().setDetailsVisible(rowIndex, false); + } } private boolean hasDetailsOpen(JsonObject row) { return row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE) - && row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE); - } - - @Override - public void closeDetails(int rowIndex) { - getWidget().setDetailsVisible(rowIndex, false); + && row.getString(GridState.JSONKEY_DETAILS_VISIBLE) != null; } }; - private final LazyDetailsScrollAdjuster lazyDetailsScrollAdjuster = new LazyDetailsScrollAdjuster(); + private final LazyDetailsScroller lazyDetailsScroller = new LazyDetailsScroller(); + private final CustomEditorHandler editorHandler = new CustomEditorHandler(); + + /* + * Initially details need to behave a bit differently to allow some + * escalator magic. + */ + private boolean initialChange; @Override @SuppressWarnings("unchecked") @@ -797,11 +668,13 @@ public class GridConnector extends AbstractHasComponentsConnector implements @Override public void scrollToEnd() { - lazyDetailsScrollAdjuster.adjustForEnd(); Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { getWidget().scrollToEnd(); + // Scrolls further if details opens. + lazyDetailsScroller.scrollToRow(dataSource.size() - 1, + ScrollDestination.END); } }); } @@ -809,11 +682,12 @@ public class GridConnector extends AbstractHasComponentsConnector implements @Override public void scrollToRow(final int row, final ScrollDestination destination) { - lazyDetailsScrollAdjuster.adjustFor(row, destination); Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { getWidget().scrollToRow(row, destination); + // Scrolls a bit further if details opens. + lazyDetailsScroller.scrollToRow(row, destination); } }); } @@ -822,59 +696,16 @@ public class GridConnector extends AbstractHasComponentsConnector implements public void recalculateColumnWidths() { getWidget().recalculateColumnWidths(); } - - @Override - @SuppressWarnings("boxing") - public void setDetailsConnectorChanges( - Set<DetailsConnectorChange> connectorChanges, int fetchId) { - customDetailsGenerator - .setDetailsConnectorChanges(connectorChanges); - - List<DetailsConnectorChange> removedFirst = new ArrayList<DetailsConnectorChange>( - connectorChanges); - Collections.sort(removedFirst, - DetailsConnectorChange.REMOVED_FIRST_COMPARATOR); - - // refresh moved/added details rows - for (DetailsConnectorChange change : removedFirst) { - Integer oldIndex = change.getOldIndex(); - Integer newIndex = change.getNewIndex(); - - assert oldIndex == null || oldIndex >= 0 : "Got an " - + "invalid old index: " + oldIndex - + " (connector: " + change.getConnector() + ")"; - assert newIndex == null || newIndex >= 0 : "Got an " - + "invalid new index: " + newIndex - + " (connector: " + change.getConnector() + ")"; - - if (oldIndex != null) { - /* Close the old/removed index */ - getWidget().setDetailsVisible(oldIndex, false); - - if (change.isShouldStillBeVisible()) { - getWidget().setDetailsVisible(oldIndex, true); - } - } - - if (newIndex != null) { - /* - * Since the component was lazy loaded, we need to - * refresh the details by toggling it. - */ - getWidget().setDetailsVisible(newIndex, false); - getWidget().setDetailsVisible(newIndex, true); - } - } - detailsConnectorFetcher.responseReceived(fetchId); - } }); - getWidget().addSelectionHandler(internalSelectionChangeHandler); - /* Item click events */ getWidget().addBodyClickHandler(itemClickHandler); getWidget().addBodyDoubleClickHandler(itemClickHandler); + /* Style Generators */ + getWidget().setCellStyleGenerator(styleGenerator); + getWidget().setRowStyleGenerator(styleGenerator); + getWidget().addSortHandler(new SortHandler<JsonObject>() { @Override public void sort(SortEvent<JsonObject> event) { @@ -899,36 +730,51 @@ public class GridConnector extends AbstractHasComponentsConnector implements } }); - getWidget().addSelectAllHandler(new SelectAllHandler<JsonObject>() { - - @Override - public void onSelectAll(SelectAllEvent<JsonObject> event) { - getRpcProxy(GridServerRpc.class).selectAll(); - } - - }); - - getWidget().setEditorHandler(new CustomEditorHandler()); + getWidget().setEditorHandler(editorHandler); getWidget().addColumnReorderHandler(columnReorderHandler); getWidget().addColumnVisibilityChangeHandler( columnVisibilityChangeHandler); + + ConnectorFocusAndBlurHandler.addHandlers(this); + getWidget().setDetailsGenerator(customDetailsGenerator); getLayoutManager().registerDependency(this, getWidget().getElement()); - layout(); - } + getWidget().addEditorEventHandler(new EditorEventHandler() { + @Override + public void onEditorOpen(EditorOpenEvent e) { + if (hasEventListener(GridConstants.EDITOR_OPEN_EVENT_ID)) { + String rowKey = getRowKey((JsonObject) e.getRow()); + getRpcProxy(GridServerRpc.class).editorOpen(rowKey); + } + } - @Override - public void onUnregister() { - customDetailsGenerator.indexToDetailsMap.clear(); + @Override + public void onEditorMove(EditorMoveEvent e) { + if (hasEventListener(GridConstants.EDITOR_MOVE_EVENT_ID)) { + String rowKey = getRowKey((JsonObject) e.getRow()); + getRpcProxy(GridServerRpc.class).editorMove(rowKey); + } + } - super.onUnregister(); + @Override + public void onEditorClose(EditorCloseEvent e) { + if (hasEventListener(GridConstants.EDITOR_CLOSE_EVENT_ID)) { + String rowKey = getRowKey((JsonObject) e.getRow()); + getRpcProxy(GridServerRpc.class).editorClose(rowKey); + } + } + }); + + layout(); } @Override public void onStateChanged(final StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); + initialChange = stateChangeEvent.isInitialStateChange(); + // Column updates if (stateChangeEvent.hasPropertyChanged("columns")) { @@ -959,19 +805,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements updateFooterFromState(getState().footer); } - // Selection - if (stateChangeEvent.hasPropertyChanged("selectionMode")) { - onSelectionModeChange(); - updateSelectDeselectAllowed(); - } else if (stateChangeEvent - .hasPropertyChanged("singleSelectDeselectAllowed")) { - updateSelectDeselectAllowed(); - } - - if (stateChangeEvent.hasPropertyChanged("selectedKeys")) { - updateSelectionFromState(); - } - // Sorting if (stateChangeEvent.hasPropertyChanged("sortColumns") || stateChangeEvent.hasPropertyChanged("sortDirs")) { @@ -998,14 +831,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements } } - private void updateSelectDeselectAllowed() { - SelectionModel<JsonObject> model = getWidget().getSelectionModel(); - if (model instanceof SelectionModel.Single<?>) { - ((SelectionModel.Single<?>) model) - .setDeselectAllowed(getState().singleSelectDeselectAllowed); - } - } - private void updateColumnOrderFromState(List<String> stateColumnOrder) { CustomGridColumn[] columns = new CustomGridColumn[stateColumnOrder .size()]; @@ -1178,20 +1003,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements } /** - * 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 @@ -1253,81 +1064,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements getWidget().setDataSource(this.dataSource); } - private void onSelectionModeChange() { - SharedSelectionMode mode = getState().selectionMode; - if (mode == null) { - getLogger().fine("ignored mode change"); - return; - } - - AbstractRowHandleSelectionModel<JsonObject> model = createSelectionModel(mode); - if (selectionModel == null - || !model.getClass().equals(selectionModel.getClass())) { - selectionModel = model; - getWidget().setSelectionModel(model); - selectedKeys.clear(); - } - } - - @OnStateChange("hasCellStyleGenerator") - private void onCellStyleGeneratorChange() { - if (getState().hasCellStyleGenerator) { - getWidget().setCellStyleGenerator(new CustomCellStyleGenerator()); - } else { - getWidget().setCellStyleGenerator(null); - } - } - - @OnStateChange("hasRowStyleGenerator") - private void onRowStyleGeneratorChange() { - if (getState().hasRowStyleGenerator) { - getWidget().setRowStyleGenerator(new CustomRowStyleGenerator()); - } else { - getWidget().setRowStyleGenerator(null); - } - } - - private void updateSelectionFromState() { - boolean changed = false; - - List<String> stateKeys = getState().selectedKeys; - - // find new deselections - for (String key : selectedKeys) { - if (!stateKeys.contains(key)) { - changed = true; - deselectByHandle(dataSource.getHandleByKey(key)); - } - } - - // find new selections - for (String key : stateKeys) { - if (!selectedKeys.contains(key)) { - changed = true; - selectByHandle(dataSource.getHandleByKey(key)); - } - } - - /* - * A defensive copy in case the collection in the state is mutated - * instead of re-assigned. - */ - selectedKeys = new LinkedHashSet<String>(stateKeys); - - /* - * We need to fire this event so that Grid is able to re-render the - * selection changes (if applicable). - */ - if (changed) { - // At least for now there's no way to send the selected and/or - // deselected row data. Some data is only stored as keys - selectionUpdatedFromState = true; - getWidget().fireEvent( - new SelectionEvent<JsonObject>(getWidget(), - (List<JsonObject>) null, null, false)); - } - } - private void onSortStateChange() { List<SortOrder> sortOrder = new ArrayList<SortOrder>(); @@ -1346,41 +1082,6 @@ public class GridConnector extends AbstractHasComponentsConnector implements return Logger.getLogger(getClass().getName()); } - @SuppressWarnings("static-method") - private AbstractRowHandleSelectionModel<JsonObject> createSelectionModel( - SharedSelectionMode mode) { - switch (mode) { - case SINGLE: - return new SelectionModelSingle<JsonObject>(); - case MULTI: - return new SelectionModelMulti<JsonObject>(); - case NONE: - return new SelectionModelNone<JsonObject>(); - default: - throw new IllegalStateException("unexpected mode value: " + mode); - } - } - - /** - * A workaround method for accessing the protected method - * {@code AbstractRowHandleSelectionModel.selectByHandle} - */ - private native void selectByHandle(RowHandle<JsonObject> handle) - /*-{ - var model = this.@com.vaadin.client.connectors.GridConnector::selectionModel; - model.@com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel::selectByHandle(*)(handle); - }-*/; - - /** - * A workaround method for accessing the protected method - * {@code AbstractRowHandleSelectionModel.deselectByHandle} - */ - private native void deselectByHandle(RowHandle<JsonObject> handle) - /*-{ - var model = this.@com.vaadin.client.connectors.GridConnector::selectionModel; - model.@com.vaadin.client.widget.grid.selection.AbstractRowHandleSelectionModel::deselectByHandle(*)(handle); - }-*/; - /** * Gets the row key for a row object. * @@ -1405,12 +1106,12 @@ public class GridConnector extends AbstractHasComponentsConnector implements @Override public void updateCaption(ComponentConnector connector) { // TODO Auto-generated method stub - } @Override public void onConnectorHierarchyChange( ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) { + customDetailsGenerator.updateConnectorHierarchy(getChildren()); } public String getColumnId(Grid.Column<?, ?> column) { @@ -1427,8 +1128,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements @Override public boolean isWorkPending() { - return detailsConnectorFetcher.isWorkPending() - || lazyDetailsScrollAdjuster.isWorkPending(); + return lazyDetailsScroller.isWorkPending(); } /** @@ -1441,4 +1141,74 @@ public class GridConnector extends AbstractHasComponentsConnector implements public DetailsListener getDetailsListener() { return detailsListener; } + + @Override + public boolean hasTooltip() { + return getState().hasDescriptions || super.hasTooltip(); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + CellReference<JsonObject> cell = getWidget().getCellReference(element); + + if (cell != null) { + JsonObject row = cell.getRow(); + if (row == null) { + return null; + } + + Column<?, JsonObject> column = cell.getColumn(); + if (!(column instanceof CustomGridColumn)) { + // Selection checkbox column + return null; + } + CustomGridColumn c = (CustomGridColumn) column; + + JsonObject cellDescriptions = row + .getObject(GridState.JSONKEY_CELLDESCRIPTION); + + if (cellDescriptions != null && cellDescriptions.hasKey(c.id)) { + return new TooltipInfo(cellDescriptions.getString(c.id)); + } else if (row.hasKey(GridState.JSONKEY_ROWDESCRIPTION)) { + return new TooltipInfo( + row.getString(GridState.JSONKEY_ROWDESCRIPTION)); + } else { + return null; + } + } + + return super.getTooltipInfo(element); + } + + /** + * Creates a concatenation of all columns errors for Editor. + * + * @since + * @return displayed error string + */ + private String getColumnErrors() { + List<String> errors = new ArrayList<String>(); + + for (Grid.Column<?, JsonObject> c : getWidget().getColumns()) { + if (!(c instanceof CustomGridColumn)) { + continue; + } + + String error = columnToErrorMessage.get(c); + if (error != null) { + errors.add(error); + } + } + + String result = ""; + Iterator<String> i = errors.iterator(); + while (i.hasNext()) { + result += i.next(); + if (i.hasNext()) { + result += ", "; + } + } + return result.isEmpty() ? null : result; + } + } diff --git a/client/src/com/vaadin/client/connectors/MultiSelectionModelConnector.java b/client/src/com/vaadin/client/connectors/MultiSelectionModelConnector.java new file mode 100644 index 0000000000..e4ad50e7ac --- /dev/null +++ b/client/src/com/vaadin/client/connectors/MultiSelectionModelConnector.java @@ -0,0 +1,383 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.connectors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.CheckBox; +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.client.renderers.ComplexRenderer; +import com.vaadin.client.renderers.Renderer; +import com.vaadin.client.widget.grid.DataAvailableEvent; +import com.vaadin.client.widget.grid.DataAvailableHandler; +import com.vaadin.client.widget.grid.events.SelectAllEvent; +import com.vaadin.client.widget.grid.events.SelectAllHandler; +import com.vaadin.client.widget.grid.selection.MultiSelectionRenderer; +import com.vaadin.client.widget.grid.selection.SelectionModel; +import com.vaadin.client.widget.grid.selection.SelectionModel.Multi; +import com.vaadin.client.widget.grid.selection.SpaceSelectHandler; +import com.vaadin.client.widgets.Grid; +import com.vaadin.client.widgets.Grid.HeaderCell; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.selection.MultiSelectionModelServerRpc; +import com.vaadin.shared.ui.grid.selection.MultiSelectionModelState; +import com.vaadin.ui.Grid.MultiSelectionModel; + +import elemental.json.JsonObject; + +/** + * Connector for server-side {@link MultiSelectionModel}. + * + * @since + * @author Vaadin Ltd + */ +@Connect(MultiSelectionModel.class) +public class MultiSelectionModelConnector extends + AbstractSelectionModelConnector<SelectionModel.Multi<JsonObject>> { + + private Multi<JsonObject> selectionModel = createSelectionModel(); + private SpaceSelectHandler<JsonObject> spaceHandler; + + @Override + protected void extend(ServerConnector target) { + getGrid().setSelectionModel(selectionModel); + spaceHandler = new SpaceSelectHandler<JsonObject>(getGrid()); + } + + @Override + public void onUnregister() { + spaceHandler.removeHandler(); + } + + @Override + protected Multi<JsonObject> createSelectionModel() { + return new MultiSelectionModel(); + } + + @Override + public MultiSelectionModelState getState() { + return (MultiSelectionModelState) super.getState(); + } + + @OnStateChange("allSelected") + void updateSelectAllCheckbox() { + if (selectionModel.getSelectionColumnRenderer() != null) { + HeaderCell cell = getGrid().getDefaultHeaderRow().getCell( + getGrid().getColumn(0)); + CheckBox widget = (CheckBox) cell.getWidget(); + widget.setValue(getState().allSelected, false); + } + } + + protected class MultiSelectionModel extends AbstractSelectionModel + implements SelectionModel.Multi.Batched<JsonObject> { + + private ComplexRenderer<Boolean> renderer = null; + private Set<RowHandle<JsonObject>> selected = new HashSet<RowHandle<JsonObject>>(); + private Set<RowHandle<JsonObject>> deselected = new HashSet<RowHandle<JsonObject>>(); + private HandlerRegistration selectAll; + private HandlerRegistration dataAvailable; + private Range availableRows; + private boolean batchSelect = false; + + @Override + public void setGrid(Grid<JsonObject> grid) { + super.setGrid(grid); + if (grid != null) { + renderer = createSelectionColumnRenderer(grid); + selectAll = getGrid().addSelectAllHandler( + new SelectAllHandler<JsonObject>() { + + @Override + public void onSelectAll( + SelectAllEvent<JsonObject> event) { + selectAll(); + } + }); + dataAvailable = getGrid().addDataAvailableHandler( + new DataAvailableHandler() { + + @Override + public void onDataAvailable(DataAvailableEvent event) { + availableRows = event.getAvailableRows(); + } + }); + } else if (renderer != null) { + renderer.destroy(); + selectAll.removeHandler(); + dataAvailable.removeHandler(); + renderer = null; + } + } + + /** + * Creates a selection column renderer. This method can be overridden to + * use a custom renderer or use {@code null} to disable the selection + * column. + * + * @param grid + * the grid for this selection model + * @return selection column renderer or {@code null} if not needed + */ + protected ComplexRenderer<Boolean> createSelectionColumnRenderer( + Grid<JsonObject> grid) { + return new MultiSelectionRenderer<JsonObject>(grid); + } + + /** + * Selects all available rows, sends request to server to select + * everything. + */ + public void selectAll() { + assert !isBeingBatchSelected() : "Can't select all in middle of a batch selection."; + + DataSource<JsonObject> dataSource = getGrid().getDataSource(); + for (int i = availableRows.getStart(); i < availableRows.getEnd(); ++i) { + final JsonObject row = dataSource.getRow(i); + if (row != null) { + RowHandle<JsonObject> handle = dataSource.getHandle(row); + markAsSelected(handle, true); + } + } + + getRpcProxy(MultiSelectionModelServerRpc.class).selectAll(); + } + + @Override + public Renderer<Boolean> getSelectionColumnRenderer() { + return renderer; + } + + /** + * {@inheritDoc} + * + * @return {@code false} if rows is empty, else {@code true} + */ + @Override + public boolean select(JsonObject... rows) { + return select(Arrays.asList(rows)); + } + + /** + * {@inheritDoc} + * + * @return {@code false} if rows is empty, else {@code true} + */ + @Override + public boolean deselect(JsonObject... rows) { + return deselect(Arrays.asList(rows)); + } + + /** + * {@inheritDoc} + * + * @return always {@code true} + */ + @Override + public boolean deselectAll() { + assert !isBeingBatchSelected() : "Can't select all in middle of a batch selection."; + + DataSource<JsonObject> dataSource = getGrid().getDataSource(); + for (int i = availableRows.getStart(); i < availableRows.getEnd(); ++i) { + final JsonObject row = dataSource.getRow(i); + if (row != null) { + RowHandle<JsonObject> handle = dataSource.getHandle(row); + markAsSelected(handle, false); + } + } + + getRpcProxy(MultiSelectionModelServerRpc.class).deselectAll(); + + return true; + } + + /** + * {@inheritDoc} + * + * @return {@code false} if rows is empty, else {@code true} + */ + @Override + public boolean select(Collection<JsonObject> rows) { + if (rows.isEmpty()) { + return false; + } + + for (JsonObject row : rows) { + RowHandle<JsonObject> rowHandle = getRowHandle(row); + if (markAsSelected(rowHandle, true)) { + selected.add(rowHandle); + } + } + + if (!isBeingBatchSelected()) { + sendSelected(); + } + return true; + } + + /** + * Marks the given row to be selected or deselected. Returns true if the + * value actually changed. + * <p> + * Note: If selection model is in batch select state, the row will be + * pinned on select. + * + * @param row + * row handle + * @param selected + * {@code true} if row should be selected; {@code false} if + * not + * @return {@code true} if selected status changed; {@code false} if not + */ + protected boolean markAsSelected(RowHandle<JsonObject> row, + boolean selected) { + if (selected && !isSelected(row.getRow())) { + row.getRow().put(GridState.JSONKEY_SELECTED, true); + } else if (!selected && isSelected(row.getRow())) { + row.getRow().remove(GridState.JSONKEY_SELECTED); + } else { + return false; + } + + row.updateRow(); + + if (isBeingBatchSelected()) { + row.pin(); + } + return true; + } + + /** + * {@inheritDoc} + * + * @return {@code false} if rows is empty, else {@code true} + */ + @Override + public boolean deselect(Collection<JsonObject> rows) { + if (rows.isEmpty()) { + return false; + } + + for (JsonObject row : rows) { + RowHandle<JsonObject> rowHandle = getRowHandle(row); + if (markAsSelected(rowHandle, false)) { + deselected.add(rowHandle); + } + } + + if (!isBeingBatchSelected()) { + sendDeselected(); + } + return true; + } + + /** + * Sends a deselect RPC call to server-side containing all deselected + * rows. Unpins any pinned rows. + */ + private void sendDeselected() { + getRpcProxy(MultiSelectionModelServerRpc.class).deselect( + getRowKeys(deselected)); + + if (isBeingBatchSelected()) { + for (RowHandle<JsonObject> row : deselected) { + row.unpin(); + } + } + + deselected.clear(); + } + + /** + * Sends a select RPC call to server-side containing all selected rows. + * Unpins any pinned rows. + */ + private void sendSelected() { + getRpcProxy(MultiSelectionModelServerRpc.class).select( + getRowKeys(selected)); + + if (isBeingBatchSelected()) { + for (RowHandle<JsonObject> row : selected) { + row.unpin(); + } + } + + selected.clear(); + } + + private List<String> getRowKeys(Set<RowHandle<JsonObject>> handles) { + List<String> keys = new ArrayList<String>(); + for (RowHandle<JsonObject> handle : handles) { + keys.add(getRowKey(handle.getRow())); + } + return keys; + } + + private Set<JsonObject> getRows(Set<RowHandle<JsonObject>> handles) { + Set<JsonObject> rows = new HashSet<JsonObject>(); + for (RowHandle<JsonObject> handle : handles) { + rows.add(handle.getRow()); + } + return rows; + } + + @Override + public void startBatchSelect() { + assert selected.isEmpty() && deselected.isEmpty() : "Row caches were not clear."; + batchSelect = true; + } + + @Override + public void commitBatchSelect() { + assert batchSelect : "Not batch selecting."; + if (!selected.isEmpty()) { + sendSelected(); + } + + if (!deselected.isEmpty()) { + sendDeselected(); + } + batchSelect = false; + } + + @Override + public boolean isBeingBatchSelected() { + return batchSelect; + } + + @Override + public Collection<JsonObject> getSelectedRowsBatch() { + return Collections.unmodifiableSet(getRows(selected)); + } + + @Override + public Collection<JsonObject> getDeselectedRowsBatch() { + return Collections.unmodifiableSet(getRows(deselected)); + } + } +} diff --git a/client/src/com/vaadin/client/connectors/NoSelectionModelConnector.java b/client/src/com/vaadin/client/connectors/NoSelectionModelConnector.java new file mode 100644 index 0000000000..b540eed6d5 --- /dev/null +++ b/client/src/com/vaadin/client/connectors/NoSelectionModelConnector.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.connectors; + +import com.vaadin.client.ServerConnector; +import com.vaadin.client.widget.grid.selection.SelectionModel; +import com.vaadin.client.widget.grid.selection.SelectionModelNone; +import com.vaadin.shared.ui.Connect; +import com.vaadin.ui.Grid.NoSelectionModel; + +import elemental.json.JsonObject; + +/** + * Connector for server-side {@link NoSelectionModel}. + */ +@Connect(NoSelectionModel.class) +public class NoSelectionModelConnector extends + AbstractSelectionModelConnector<SelectionModel<JsonObject>> { + + @Override + protected void extend(ServerConnector target) { + getGrid().setSelectionModel(createSelectionModel()); + } + + @Override + protected SelectionModel<JsonObject> createSelectionModel() { + return new SelectionModelNone<JsonObject>(); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java index 627ee74eca..5daa02c3bf 100644 --- a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java @@ -17,6 +17,7 @@ package com.vaadin.client.connectors; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import com.vaadin.client.ServerConnector; @@ -64,14 +65,6 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { * @see GridState#JSONKEY_DETAILS_VISIBLE */ void reapplyDetailsVisibility(int rowIndex, JsonObject row); - - /** - * Closes details for a row. - * - * @param rowIndex - * the index of the row for which to close details - */ - void closeDetails(int rowIndex); } public class RpcDataSource extends AbstractRemoteDataSource<JsonObject> { @@ -104,15 +97,28 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { public void resetDataAndSize(int size) { RpcDataSource.this.resetDataAndSize(size); } + + @Override + public void updateRowData(JsonArray rowArray) { + for (int i = 0; i < rowArray.length(); ++i) { + RpcDataSource.this.updateRowData(rowArray.getObject(i)); + } + } }); } private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class); private DetailsListener detailsListener; + private JsonArray droppedRowKeys = Json.createArray(); @Override protected void requestRows(int firstRowIndex, int numberOfRows, RequestRowsCallback<JsonObject> callback) { + if (droppedRowKeys.length() > 0) { + rpcProxy.dropRows(droppedRowKeys); + droppedRowKeys = Json.createArray(); + } + /* * If you're looking at this code because you want to learn how to * use AbstactRemoteDataSource, please look somewhere else instead. @@ -184,23 +190,15 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { } @Override - protected void pinHandle(RowHandleImpl handle) { - // Server only knows if something is pinned or not. No need to pin - // multiple times. - boolean pinnedBefore = handle.isPinned(); - super.pinHandle(handle); - if (!pinnedBefore) { - rpcProxy.setPinned(getRowKey(handle.getRow()), true); - } - } - - @Override protected void unpinHandle(RowHandleImpl handle) { // Row data is no longer available after it has been unpinned. String key = getRowKey(handle.getRow()); super.unpinHandle(handle); if (!handle.isPinned()) { - rpcProxy.setPinned(key, false); + if (indexOfKey(key) == -1) { + // Row out of view has been unpinned. drop it + droppedRowKeys.set(droppedRowKeys.length(), key); + } } } @@ -222,9 +220,25 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { } } + /** + * Updates row data based on row key. + * + * @since 7.6 + * @param row + * new row object + */ + protected void updateRowData(JsonObject row) { + int index = indexOfKey(getRowKey(row)); + if (index >= 0) { + setRowData(index, Collections.singletonList(row)); + } + } + @Override - protected void onDropFromCache(int rowIndex) { - detailsListener.closeDetails(rowIndex); + protected void onDropFromCache(int rowIndex, JsonObject row) { + if (!((RowHandleImpl) getHandle(row)).isPinned()) { + droppedRowKeys.set(droppedRowKeys.length(), getRowKey(row)); + } } } diff --git a/client/src/com/vaadin/client/connectors/SingleSelectionModelConnector.java b/client/src/com/vaadin/client/connectors/SingleSelectionModelConnector.java new file mode 100644 index 0000000000..7c66903c2c --- /dev/null +++ b/client/src/com/vaadin/client/connectors/SingleSelectionModelConnector.java @@ -0,0 +1,148 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.connectors; + +import com.vaadin.client.ServerConnector; +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.data.DataSource.RowHandle; +import com.vaadin.client.renderers.Renderer; +import com.vaadin.client.widget.grid.selection.ClickSelectHandler; +import com.vaadin.client.widget.grid.selection.SelectionModel; +import com.vaadin.client.widget.grid.selection.SelectionModel.Single; +import com.vaadin.client.widget.grid.selection.SpaceSelectHandler; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.selection.SingleSelectionModelServerRpc; +import com.vaadin.shared.ui.grid.selection.SingleSelectionModelState; +import com.vaadin.ui.Grid.SingleSelectionModel; + +import elemental.json.JsonObject; + +/** + * Connector for server-side {@link SingleSelectionModel}. + * + * @since + * @author Vaadin Ltd + */ +@Connect(SingleSelectionModel.class) +public class SingleSelectionModelConnector extends + AbstractSelectionModelConnector<SelectionModel.Single<JsonObject>> { + + private SpaceSelectHandler<JsonObject> spaceHandler; + private ClickSelectHandler<JsonObject> clickHandler; + private Single<JsonObject> selectionModel = createSelectionModel(); + + @Override + protected void extend(ServerConnector target) { + getGrid().setSelectionModel(selectionModel); + spaceHandler = new SpaceSelectHandler<JsonObject>(getGrid()); + clickHandler = new ClickSelectHandler<JsonObject>(getGrid()); + } + + @Override + public SingleSelectionModelState getState() { + return (SingleSelectionModelState) super.getState(); + } + + @Override + public void onUnregister() { + spaceHandler.removeHandler(); + clickHandler.removeHandler(); + + super.onUnregister(); + } + + @Override + protected Single<JsonObject> createSelectionModel() { + return new SingleSelectionModel(); + } + + @OnStateChange("deselectAllowed") + void updateDeselectAllowed() { + selectionModel.setDeselectAllowed(getState().deselectAllowed); + } + + /** + * SingleSelectionModel without a selection column renderer. + */ + public class SingleSelectionModel extends AbstractSelectionModel implements + SelectionModel.Single<JsonObject> { + + private RowHandle<JsonObject> selectedRow; + private boolean deselectAllowed; + + @Override + public Renderer<Boolean> getSelectionColumnRenderer() { + return null; + } + + @Override + public boolean select(JsonObject row) { + boolean changed = false; + if ((row == null && isDeselectAllowed()) + || (row != null && !getRowHandle(row).equals(selectedRow))) { + if (selectedRow != null) { + selectedRow.getRow().remove(GridState.JSONKEY_SELECTED); + selectedRow.updateRow(); + selectedRow.unpin(); + selectedRow = null; + changed = true; + } + + if (row != null) { + selectedRow = getRowHandle(row); + selectedRow.pin(); + selectedRow.getRow().put(GridState.JSONKEY_SELECTED, true); + selectedRow.updateRow(); + changed = true; + } + } + + if (changed) { + getRpcProxy(SingleSelectionModelServerRpc.class).select( + getRowKey(row)); + } + + return changed; + } + + @Override + public boolean deselect(JsonObject row) { + if (getRowHandle(row).equals(selectedRow)) { + select(null); + } + return false; + } + + @Override + public JsonObject getSelectedRow() { + throw new UnsupportedOperationException( + "This client-side selection model " + + getClass().getSimpleName() + + " does not know selected row."); + } + + @Override + public void setDeselectAllowed(boolean deselectAllowed) { + this.deselectAllowed = deselectAllowed; + } + + @Override + public boolean isDeselectAllowed() { + return deselectAllowed; + } + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index 459127c9b4..58cd5c5f19 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -16,8 +16,6 @@ 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; @@ -120,12 +118,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { @Override public T getRow() throws IllegalStateException { - if (isPinned()) { - return row; - } else { - throw new IllegalStateException("The row handle for key " + key - + " was not pinned"); - } + return row; } public boolean isPinned() { @@ -197,7 +190,6 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { private Map<Object, Integer> pinnedCounts = new HashMap<Object, Integer>(); private Map<Object, RowHandleImpl> pinnedRows = new HashMap<Object, RowHandleImpl>(); - protected Collection<T> temporarilyPinnedRows = Collections.emptySet(); // Size not yet known private int size = -1; @@ -287,8 +279,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { * Simple case: no overlap between cached data and needed data. * Clear the cache and request new data */ - indexToRowMap.clear(); - keyToIndexMap.clear(); + dropFromCache(cached); cached = Range.between(0, 0); handleMissingRows(getMaxCacheRange()); @@ -330,25 +321,48 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { private void dropFromCache(Range range) { for (int i = range.getStart(); i < range.getEnd(); i++) { + // Called after dropping from cache. Dropped row is passed as a + // parameter, but is no longer present in the DataSource T removed = indexToRowMap.remove(Integer.valueOf(i)); + onDropFromCache(i, removed); keyToIndexMap.remove(getRowKey(removed)); - - onDropFromCache(i); } } /** - * A hook that can be overridden to do something whenever a row is dropped - * from the cache. + * A hook that can be overridden to do something whenever a row has been + * dropped from the cache. DataSource no longer has anything in the given + * index. + * <p> + * NOTE: This method has been replaced. Override + * {@link #onDropFromCache(int, Object)} instead of this method. * * @since 7.5.0 * @param rowIndex * the index of the dropped row + * @deprecated replaced by {@link #onDropFromCache(int, Object)} */ + @Deprecated protected void onDropFromCache(int rowIndex) { // noop } + /** + * A hook that can be overridden to do something whenever a row has been + * dropped from the cache. DataSource no longer has anything in the given + * index. + * + * @since 7.6 + * @param rowIndex + * the index of the dropped row + * @param removed + * the removed row object + */ + protected void onDropFromCache(int rowIndex, T removed) { + // Call old version as a fallback (someone might have used it) + onDropFromCache(rowIndex); + } + private void handleMissingRows(Range range) { if (range.isEmpty()) { return; @@ -479,6 +493,15 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { * updated before the widget settings. Support for this will be * implemented later on. */ + + // Run a dummy drop from cache for unused rows. + for (int i = 0; i < partition[0].length(); ++i) { + onDropFromCache(i + partition[0].getStart(), rowData.get(i)); + } + + for (int i = 0; i < partition[2].length(); ++i) { + onDropFromCache(i + partition[2].getStart(), rowData.get(i)); + } } // Eventually check whether all needed rows are now available @@ -732,4 +755,12 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { dataChangeHandler.resetDataAndSize(newSize); } } + + protected int indexOfKey(Object rowKey) { + if (!keyToIndexMap.containsKey(rowKey)) { + return -1; + } else { + return keyToIndexMap.get(rowKey); + } + } } diff --git a/client/src/com/vaadin/client/debug/internal/VDebugWindow.java b/client/src/com/vaadin/client/debug/internal/VDebugWindow.java index b543c23e4d..fbc838f861 100644 --- a/client/src/com/vaadin/client/debug/internal/VDebugWindow.java +++ b/client/src/com/vaadin/client/debug/internal/VDebugWindow.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.Date; import com.google.gwt.core.client.Duration; +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.Element; import com.google.gwt.dom.client.NativeEvent; @@ -698,22 +700,32 @@ public final class VDebugWindow extends VOverlay { public void init() { show(); - readStoredState(); - Window.addResizeHandler(new com.google.gwt.event.logical.shared.ResizeHandler() { + /* + * Finalize initialization when all entry points have had the chance to + * e.g. register new sections. + */ + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @Override + public void execute() { + readStoredState(); - Timer t = new Timer() { - @Override - public void run() { - applyPositionAndSize(); - } - }; + Window.addResizeHandler(new com.google.gwt.event.logical.shared.ResizeHandler() { - @Override - public void onResize(ResizeEvent event) { - t.cancel(); - // TODO less - t.schedule(1000); + Timer t = new Timer() { + @Override + public void run() { + applyPositionAndSize(); + } + }; + + @Override + public void onResize(ResizeEvent event) { + t.cancel(); + // TODO less + t.schedule(1000); + } + }); } }); } diff --git a/client/src/com/vaadin/client/extensions/ResponsiveConnector.java b/client/src/com/vaadin/client/extensions/ResponsiveConnector.java index 621c69788c..ae330be8f4 100644 --- a/client/src/com/vaadin/client/extensions/ResponsiveConnector.java +++ b/client/src/com/vaadin/client/extensions/ResponsiveConnector.java @@ -205,7 +205,7 @@ public class ResponsiveConnector extends AbstractExtensionConnector implements // Get all the rulesets from the stylesheet var theRules = new Array(); - var IE = @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isIE()(); + var IEOrEdge = @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isIE()() || @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isEdge()(); var IE8 = @com.vaadin.client.BrowserInfo::get()().@com.vaadin.client.BrowserInfo::isIE8()(); try { @@ -263,8 +263,8 @@ public class ResponsiveConnector extends AbstractExtensionConnector implements // Array of all of the separate selectors in this ruleset var haystack = rule.selectorText.split(","); - // IE parses CSS like .class[attr="val"] into [attr="val"].class so we need to check for both - var selectorRegEx = IE ? /\[.*\]([\.|#]\S+)/ : /([\.|#]\S+?)\[.*\]/; + // IE/Edge parses CSS like .class[attr="val"] into [attr="val"].class so we need to check for both + var selectorRegEx = IEOrEdge ? /\[.*\]([\.|#]\S+)/ : /([\.|#]\S+?)\[.*\]/; // Loop all the selectors in this ruleset for(var k = 0, len2 = haystack.length; k < len2; k++) { diff --git a/client/src/com/vaadin/client/ui/AbstractConnector.java b/client/src/com/vaadin/client/ui/AbstractConnector.java index a20c3463c2..7f8d6c2b14 100644 --- a/client/src/com/vaadin/client/ui/AbstractConnector.java +++ b/client/src/com/vaadin/client/ui/AbstractConnector.java @@ -25,7 +25,7 @@ import java.util.Set; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerManager; -import com.google.web.bindery.event.shared.HandlerRegistration; +import com.google.gwt.event.shared.HandlerRegistration; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.FastStringMap; import com.vaadin.client.FastStringSet; diff --git a/client/src/com/vaadin/client/ui/ConnectorFocusAndBlurHandler.java b/client/src/com/vaadin/client/ui/ConnectorFocusAndBlurHandler.java new file mode 100644 index 0000000000..8272f99d25 --- /dev/null +++ b/client/src/com/vaadin/client/ui/ConnectorFocusAndBlurHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS 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; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.EventHelper; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.communication.StateChangeEvent.StateChangeHandler; +import com.vaadin.shared.EventId; +import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; + +/** + * A handler for focus and blur events which uses {@link FocusAndBlurServerRpc} + * to transmit received events to the server. Events are only handled if there + * is a corresponding listener on the server side. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public class ConnectorFocusAndBlurHandler implements StateChangeHandler, + FocusHandler, BlurHandler { + + private final AbstractComponentConnector connector; + private final Widget widget; + private HandlerRegistration focusRegistration = null; + private HandlerRegistration blurRegistration = null; + + public static void addHandlers(AbstractComponentConnector connector) { + addHandlers(connector, connector.getWidget()); + } + + public static void addHandlers(AbstractComponentConnector connector, + Widget widget) { + connector.addStateChangeHandler("registeredEventListeners", + new ConnectorFocusAndBlurHandler(connector, widget)); + } + + private ConnectorFocusAndBlurHandler(AbstractComponentConnector connector, + Widget widget) { + this.connector = connector; + this.widget = widget; + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + focusRegistration = EventHelper.updateHandler(connector, this, + EventId.FOCUS, focusRegistration, FocusEvent.getType(), widget); + blurRegistration = EventHelper.updateHandler(connector, this, + EventId.BLUR, blurRegistration, BlurEvent.getType(), widget); + } + + @Override + public void onFocus(FocusEvent event) { + // updateHandler ensures that this is called only when + // there is a listener on the server side + getRpc().focus(); + } + + @Override + public void onBlur(BlurEvent event) { + // updateHandler ensures that this is called only when + // there is a listener on the server side + getRpc().blur(); + } + + private FocusAndBlurServerRpc getRpc() { + return connector.getRpcProxy(FocusAndBlurServerRpc.class); + } +} diff --git a/client/src/com/vaadin/client/ui/FocusableScrollPanel.java b/client/src/com/vaadin/client/ui/FocusableScrollPanel.java index 9dd9c17675..1ac5a08ccd 100644 --- a/client/src/com/vaadin/client/ui/FocusableScrollPanel.java +++ b/client/src/com/vaadin/client/ui/FocusableScrollPanel.java @@ -74,7 +74,7 @@ public class FocusableScrollPanel extends SimpleFocusablePanel implements style.setPosition(Position.FIXED); style.setTop(0, Unit.PX); style.setLeft(0, Unit.PX); - if (browserInfo.isIE()) { + if (browserInfo.isIE() || browserInfo.isEdge()) { // for #15294: artificially hide little bit more the // focusElement, otherwise IE will make the window to scroll // into it when focused diff --git a/client/src/com/vaadin/client/ui/VButton.java b/client/src/com/vaadin/client/ui/VButton.java index bf321f7f00..2eb967c4fa 100644 --- a/client/src/com/vaadin/client/ui/VButton.java +++ b/client/src/com/vaadin/client/ui/VButton.java @@ -23,7 +23,6 @@ import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FocusWidget; @@ -94,8 +93,6 @@ public class VButton extends FocusWidget implements ClickHandler { /** For internal use only. May be removed or replaced in the future. */ public int clickShortcut = 0; - private HandlerRegistration focusHandlerRegistration; - private HandlerRegistration blurHandlerRegistration; private long lastClickTime = 0; public VButton() { diff --git a/client/src/com/vaadin/client/ui/VFilterSelect.java b/client/src/com/vaadin/client/ui/VFilterSelect.java index 7951759fa2..cf03382333 100644 --- a/client/src/com/vaadin/client/ui/VFilterSelect.java +++ b/client/src/com/vaadin/client/ui/VFilterSelect.java @@ -65,6 +65,7 @@ import com.vaadin.client.BrowserInfo; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ComputedStyle; import com.vaadin.client.ConnectorMap; +import com.vaadin.client.DeferredWorker; import com.vaadin.client.Focusable; import com.vaadin.client.UIDL; import com.vaadin.client.VConsole; @@ -90,7 +91,7 @@ import com.vaadin.shared.util.SharedUtil; public class VFilterSelect extends Composite implements Field, KeyDownHandler, KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable, SubPartAware, HandlesAriaCaption, HandlesAriaInvalid, - HandlesAriaRequired { + HandlesAriaRequired, DeferredWorker { /** * Represents a suggestion in the suggestion popup box @@ -417,7 +418,9 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, selectPrevPage(); } else { - selectItem(menu.getItems().get(menu.getItems().size() - 1)); + if (!menu.getItems().isEmpty()) { + selectLastItem(); + } } } @@ -596,7 +599,8 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, + "]"); Element menuFirstChild = menu.getElement().getFirstChildElement(); - final int naturalMenuWidth = menuFirstChild.getOffsetWidth(); + final int naturalMenuWidth = WidgetUtil + .getRequiredWidth(menuFirstChild); if (popupOuterPadding == -1) { popupOuterPadding = WidgetUtil @@ -608,13 +612,21 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, menuFirstChild.getStyle().setWidth(100, Unit.PCT); } - if (BrowserInfo.get().isIE()) { + if (BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 11) { + // Must take margin,border,padding manually into account for + // menu element as we measure the element child and set width to + // the element parent + double naturalMenuOuterWidth = WidgetUtil + .getRequiredWidthDouble(menuFirstChild) + + getMarginBorderPaddingWidth(menu.getElement()); + /* * IE requires us to specify the width for the container * element. Otherwise it will be 100% wide */ - int rootWidth = Math.max(desiredWidth, naturalMenuWidth) - - popupOuterPadding; + double rootWidth = Math.max(desiredWidth - popupOuterPadding, + naturalMenuOuterWidth); getContainerElement().getStyle().setWidth(rootWidth, Unit.PX); } @@ -1280,6 +1292,12 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, sinkEvents(Event.ONPASTE); } + private static double getMarginBorderPaddingWidth(Element element) { + final ComputedStyle s = new ComputedStyle(element); + return s.getMarginWidth() + s.getBorderWidth() + s.getPaddingWidth(); + + } + /* * (non-Javadoc) * @@ -2185,11 +2203,15 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, @Override public com.google.gwt.user.client.Element getSubPartElement(String subPart) { - if ("textbox".equals(subPart)) { + String[] parts = subPart.split("/"); + if ("textbox".equals(parts[0])) { return tb.getElement(); - } else if ("button".equals(subPart)) { + } else if ("button".equals(parts[0])) { return popupOpener.getElement(); - } else if ("popup".equals(subPart) && suggestionPopup.isAttached()) { + } else if ("popup".equals(parts[0]) && suggestionPopup.isAttached()) { + if (parts.length == 2) { + return suggestionPopup.menu.getSubPartElement(parts[1]); + } return suggestionPopup.getElement(); } return null; @@ -2233,4 +2255,10 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, selectPopupItemWhenResponseIsReceived = Select.NONE; } + @Override + public boolean isWorkPending() { + return waitingForFilteringResponse + || suggestionPopup.lazyPageScroller.isRunning(); + } + } diff --git a/client/src/com/vaadin/client/ui/VFormLayout.java b/client/src/com/vaadin/client/ui/VFormLayout.java index 84a9b4f3dd..bcbb3ebf7b 100644 --- a/client/src/com/vaadin/client/ui/VFormLayout.java +++ b/client/src/com/vaadin/client/ui/VFormLayout.java @@ -22,7 +22,6 @@ import java.util.List; import com.google.gwt.aria.client.Roles; import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.DOM; @@ -327,22 +326,6 @@ public class VFormLayout extends SimplePanel { requiredFieldIndicator = null; } } - - // Workaround for IE weirdness, sometimes returns bad height in some - // circumstances when Caption is empty. See #1444 - // IE7 bugs more often. I wonder what happens when IE8 arrives... - // FIXME: This could be unnecessary for IE8+ - if (BrowserInfo.get().isIE()) { - if (isEmpty) { - setHeight("0px"); - getElement().getStyle().setOverflow(Overflow.HIDDEN); - } else { - setHeight(""); - getElement().getStyle().clearOverflow(); - } - - } - } /** diff --git a/client/src/com/vaadin/client/ui/VRichTextArea.java b/client/src/com/vaadin/client/ui/VRichTextArea.java index 3f63f38067..cb4482f7cb 100644 --- a/client/src/com/vaadin/client/ui/VRichTextArea.java +++ b/client/src/com/vaadin/client/ui/VRichTextArea.java @@ -322,7 +322,7 @@ public class VRichTextArea extends Composite implements Field, KeyPressHandler, if ("<br>".equals(result)) { result = ""; } - } else if (browser.isWebkit()) { + } else if (browser.isWebkit() || browser.isEdge()) { if ("<br>".equals(result) || "<div><br></div>".equals(result)) { result = ""; } diff --git a/client/src/com/vaadin/client/ui/VScrollTable.java b/client/src/com/vaadin/client/ui/VScrollTable.java index 2724daae77..6bb8f063a6 100644 --- a/client/src/com/vaadin/client/ui/VScrollTable.java +++ b/client/src/com/vaadin/client/ui/VScrollTable.java @@ -1295,8 +1295,12 @@ public class VScrollTable extends FlowPanel implements HasWidgets, if (uidl.hasVariable("selected")) { final Set<String> selectedKeys = uidl .getStringArrayVariableAsSet("selected"); - removeUnselectedRowKeys(selectedKeys); - + // Do not update focus if there is a single selected row + // that is the same as the previous selection. This prevents + // unwanted scrolling (#18247). + boolean rowsUnSelected = removeUnselectedRowKeys(selectedKeys); + boolean updateFocus = rowsUnSelected || selectedRowKeys.size() == 0 + || focusedRow == null; if (scrollBody != null) { Iterator<Widget> iterator = scrollBody.iterator(); while (iterator.hasNext()) { @@ -1313,7 +1317,7 @@ public class VScrollTable extends FlowPanel implements HasWidgets, selected = true; keyboardSelectionOverRowFetchInProgress = true; } - if (selected && selectedKeys.size() == 1) { + if (selected && selectedKeys.size() == 1 && updateFocus) { /* * If a single item is selected, move focus to the * selected row. (#10522) @@ -1338,14 +1342,14 @@ public class VScrollTable extends FlowPanel implements HasWidgets, return keyboardSelectionOverRowFetchInProgress; } - private void removeUnselectedRowKeys(final Set<String> selectedKeys) { + private boolean removeUnselectedRowKeys(final Set<String> selectedKeys) { List<String> unselectedKeys = new ArrayList<String>(0); for (String key : selectedRowKeys) { if (!selectedKeys.contains(key)) { unselectedKeys.add(key); } } - selectedRowKeys.removeAll(unselectedKeys); + return selectedRowKeys.removeAll(unselectedKeys); } /** For internal use only. May be removed or replaced in the future. */ @@ -3629,6 +3633,12 @@ public class VScrollTable extends FlowPanel implements HasWidgets, } } else { c.setText(caption); + if (BrowserInfo.get().isIE10()) { + // IE10 can some times define min-height to include + // padding when setting the text... + // See https://dev.vaadin.com/ticket/15169 + WidgetUtil.forceIERedraw(c.getElement()); + } } c.setSorted(false); diff --git a/client/src/com/vaadin/client/ui/VTabsheet.java b/client/src/com/vaadin/client/ui/VTabsheet.java index ded9977f5e..e196870348 100644 --- a/client/src/com/vaadin/client/ui/VTabsheet.java +++ b/client/src/com/vaadin/client/ui/VTabsheet.java @@ -61,11 +61,12 @@ import com.google.gwt.user.client.ui.impl.FocusImpl; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ComputedStyle; import com.vaadin.client.Focusable; import com.vaadin.client.TooltipInfo; -import com.vaadin.client.WidgetUtil; import com.vaadin.client.VCaption; import com.vaadin.client.VTooltip; +import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.ComponentConstants; @@ -1227,8 +1228,13 @@ public class VTabsheet extends VTabsheetBase implements Focusable, SubPartAware public void updateContentNodeHeight() { if (!isDynamicHeight()) { int contentHeight = getOffsetHeight(); - contentHeight -= DOM.getElementPropertyInt(deco, "offsetHeight"); + contentHeight -= deco.getOffsetHeight(); contentHeight -= tb.getOffsetHeight(); + + ComputedStyle cs = new ComputedStyle(contentNode); + contentHeight -= Math.ceil(cs.getPaddingHeight()); + contentHeight -= Math.ceil(cs.getBorderHeight()); + if (contentHeight < 0) { contentHeight = 0; } diff --git a/client/src/com/vaadin/client/ui/VTextArea.java b/client/src/com/vaadin/client/ui/VTextArea.java index 50930f2fee..bb3d3a476b 100644 --- a/client/src/com/vaadin/client/ui/VTextArea.java +++ b/client/src/com/vaadin/client/ui/VTextArea.java @@ -224,16 +224,16 @@ public class VTextArea extends VTextField implements DragImageModifier { protected boolean browserSupportsMaxLengthAttribute() { BrowserInfo info = BrowserInfo.get(); - if (info.isFirefox() && info.isBrowserVersionNewerOrEqual(4, 0)) { + if (info.isFirefox()) { return true; } - if (info.isSafari() && info.isBrowserVersionNewerOrEqual(5, 0)) { + if (info.isSafari()) { return true; } - if (info.isIE() && info.isBrowserVersionNewerOrEqual(10, 0)) { + if (info.isIE10() || info.isIE11() || info.isEdge()) { return true; } - if (info.isAndroid() && info.isBrowserVersionNewerOrEqual(2, 3)) { + if (info.isAndroid()) { return true; } return false; diff --git a/client/src/com/vaadin/client/ui/VTree.java b/client/src/com/vaadin/client/ui/VTree.java index 8729de4a43..846b16d0cb 100644 --- a/client/src/com/vaadin/client/ui/VTree.java +++ b/client/src/com/vaadin/client/ui/VTree.java @@ -179,7 +179,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, @Override public void execute() { - Util.notifyParentOfSizeChange(VTree.this, true); + doLayout(); } }); @@ -969,7 +969,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, open = state; if (!rendering) { - Util.notifyParentOfSizeChange(VTree.this, false); + doLayout(); } } @@ -2239,4 +2239,15 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, com.google.gwt.user.client.Element captionElement) { AriaHelper.bindCaption(body, captionElement); } + + /** + * Tell LayoutManager that a layout is needed later for this VTree + */ + private void doLayout() { + // IE8 needs a hack to measure the tree again after update + WidgetUtil.forceIE8Redraw(getElement()); + + // This calls LayoutManager setNeedsMeasure and layoutNow + Util.notifyParentOfSizeChange(this, false); + } } diff --git a/client/src/com/vaadin/client/ui/VUI.java b/client/src/com/vaadin/client/ui/VUI.java index 0c1b83ab0f..963d83a6e6 100644 --- a/client/src/com/vaadin/client/ui/VUI.java +++ b/client/src/com/vaadin/client/ui/VUI.java @@ -18,7 +18,6 @@ package com.vaadin.client.ui; import java.util.ArrayList; -import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.HasScrollHandlers; @@ -44,8 +43,8 @@ import com.vaadin.client.ConnectorMap; import com.vaadin.client.Focusable; import com.vaadin.client.LayoutManager; import com.vaadin.client.Profiler; -import com.vaadin.client.WidgetUtil; import com.vaadin.client.VConsole; +import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler; import com.vaadin.client.ui.ui.UIConnector; @@ -515,13 +514,6 @@ public class VUI extends SimplePanel implements ResizeHandler, public void focusStoredElement() { if (storedFocus != null) { storedFocus.focus(); - - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - storedFocus.focus(); - } - }); } } diff --git a/client/src/com/vaadin/client/ui/VWindow.java b/client/src/com/vaadin/client/ui/VWindow.java index c5cab8ff6b..2d2d6ecee1 100644 --- a/client/src/com/vaadin/client/ui/VWindow.java +++ b/client/src/com/vaadin/client/ui/VWindow.java @@ -44,8 +44,6 @@ import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; -import com.google.gwt.event.dom.client.KeyUpEvent; -import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.event.shared.HandlerRegistration; @@ -79,8 +77,7 @@ import com.vaadin.shared.ui.window.WindowRole; * @author Vaadin Ltd */ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, - ScrollHandler, KeyDownHandler, KeyUpHandler, FocusHandler, BlurHandler, - Focusable { + ScrollHandler, KeyDownHandler, FocusHandler, BlurHandler, Focusable { private static ArrayList<VWindow> windowOrder = new ArrayList<VWindow>(); @@ -221,7 +218,6 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, constructDOM(); contentPanel.addScrollHandler(this); contentPanel.addKeyDownHandler(this); - contentPanel.addKeyUpHandler(this); contentPanel.addFocusHandler(this); contentPanel.addBlurHandler(this); } @@ -562,17 +558,10 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } private static void focusTopmostModalWindow() { - // If we call focus() directly without scheduling, it does not work in - // IE and FF. - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - VWindow topmost = getTopmostWindow(); - if ((topmost != null) && (topmost.vaadinModality)) { - topmost.focus(); - } - } - }); + VWindow topmost = getTopmostWindow(); + if ((topmost != null) && (topmost.vaadinModality)) { + topmost.focus(); + } } @Override @@ -762,11 +751,9 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, modalityCurtain.removeFromParent(); - if (BrowserInfo.get().isIE()) { - // IE leaks memory in certain cases unless we release the reference - // (#9197) - modalityCurtain = null; - } + // IE leaks memory in certain cases unless we release the reference + // (#9197) + modalityCurtain = null; } /* @@ -1353,13 +1340,6 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } @Override - public void onKeyUp(KeyUpEvent event) { - if (isClosable() && event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { - onCloseClick(); - } - } - - @Override public void onBlur(BlurEvent event) { if (client.hasEventListeners(this, EventId.BLUR)) { client.updateVariable(id, EventId.BLUR, "", true); @@ -1375,7 +1355,11 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, @Override public void focus() { - contentPanel.focus(); + // We don't want to use contentPanel.focus() as that will use a timer in + // Chrome/Safari and ultimately run focus events in the wrong order when + // opening a modal window and focusing some other component at the same + // time + contentPanel.getElement().focus(); } private int getDecorationHeight() { diff --git a/client/src/com/vaadin/client/ui/button/ButtonConnector.java b/client/src/com/vaadin/client/ui/button/ButtonConnector.java index 2d13d62a91..2c2006e19b 100644 --- a/client/src/com/vaadin/client/ui/button/ButtonConnector.java +++ b/client/src/com/vaadin/client/ui/button/ButtonConnector.java @@ -16,24 +16,17 @@ package com.vaadin.client.ui.button; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; -import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; -import com.vaadin.client.EventHelper; import com.vaadin.client.MouseEventDetailsBuilder; import com.vaadin.client.VCaption; import com.vaadin.client.annotations.OnStateChange; -import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; import com.vaadin.client.ui.Icon; import com.vaadin.client.ui.VButton; import com.vaadin.shared.MouseEventDetails; -import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.Connect.LoadStyle; import com.vaadin.shared.ui.button.ButtonServerRpc; @@ -42,10 +35,7 @@ import com.vaadin.ui.Button; @Connect(value = Button.class, loadStyle = LoadStyle.EAGER) public class ButtonConnector extends AbstractComponentConnector implements - BlurHandler, FocusHandler, ClickHandler { - - private HandlerRegistration focusHandlerRegistration = null; - private HandlerRegistration blurHandlerRegistration = null; + ClickHandler { @Override public boolean delegateCaptionHandling() { @@ -57,6 +47,7 @@ public class ButtonConnector extends AbstractComponentConnector implements super.init(); getWidget().addClickHandler(this); getWidget().client = getConnection(); + ConnectorFocusAndBlurHandler.addHandlers(this); } @OnStateChange("errorMessage") @@ -90,15 +81,6 @@ public class ButtonConnector extends AbstractComponentConnector implements } } - @Override - public void onStateChanged(StateChangeEvent stateChangeEvent) { - super.onStateChanged(stateChangeEvent); - focusHandlerRegistration = EventHelper.updateFocusHandler(this, - focusHandlerRegistration); - blurHandlerRegistration = EventHelper.updateBlurHandler(this, - blurHandlerRegistration); - } - @OnStateChange({ "caption", "captionAsHtml" }) void setCaption() { VCaption.setCaptionText(getWidget().captionElement, getState()); @@ -127,20 +109,6 @@ public class ButtonConnector extends AbstractComponentConnector implements } @Override - public void onFocus(FocusEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).focus(); - } - - @Override - public void onBlur(BlurEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).blur(); - } - - @Override public void onClick(ClickEvent event) { if (getState().disableOnClick) { // Simulate getting disabled from the server without waiting for the diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java index 1a54fe0454..55834397d3 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java @@ -268,8 +268,13 @@ public class DateCellDayEvent extends FocusableHTML implements } int endX = event.getClientX(); int endY = event.getClientY(); - int xDiff = startX - endX; - int yDiff = startY - endY; + int xDiff = 0, yDiff = 0; + if (startX != -1 && startY != -1) { + // Drag started + xDiff = startX - endX; + yDiff = startY - endY; + } + startX = -1; startY = -1; mouseMoveStarted = false; diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java index 3bf6930933..158241337b 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java @@ -392,8 +392,11 @@ public class SimpleDayCell extends FocusableFlowPanel implements int endX = event.getClientX(); int endY = event.getClientY(); - int xDiff = startX - endX; - int yDiff = startY - endY; + int xDiff = 0, yDiff = 0; + if (startX != -1 && startY != -1) { + xDiff = startX - endX; + yDiff = startY - endY; + } startX = -1; startY = -1; prevDayDiff = 0; diff --git a/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java b/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java index 9e689f3314..8cfcf7feb1 100644 --- a/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java +++ b/client/src/com/vaadin/client/ui/checkbox/CheckBoxConnector.java @@ -16,25 +16,19 @@ package com.vaadin.client.ui.checkbox; import com.google.gwt.dom.client.Style.Display; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; -import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; -import com.vaadin.client.EventHelper; import com.vaadin.client.MouseEventDetailsBuilder; import com.vaadin.client.VCaption; import com.vaadin.client.VTooltip; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractFieldConnector; +import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; import com.vaadin.client.ui.Icon; import com.vaadin.client.ui.VCheckBox; import com.vaadin.shared.MouseEventDetails; -import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc; import com.vaadin.shared.ui.checkbox.CheckBoxState; @@ -42,10 +36,7 @@ import com.vaadin.ui.CheckBox; @Connect(CheckBox.class) public class CheckBoxConnector extends AbstractFieldConnector implements - FocusHandler, BlurHandler, ClickHandler { - - private HandlerRegistration focusHandlerRegistration; - private HandlerRegistration blurHandlerRegistration; + ClickHandler { @Override public boolean delegateCaptionHandling() { @@ -55,21 +46,18 @@ public class CheckBoxConnector extends AbstractFieldConnector implements @Override protected void init() { super.init(); + getWidget().addClickHandler(this); getWidget().client = getConnection(); getWidget().id = getConnectorId(); + ConnectorFocusAndBlurHandler.addHandlers(this); } @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); - focusHandlerRegistration = EventHelper.updateFocusHandler(this, - focusHandlerRegistration); - blurHandlerRegistration = EventHelper.updateBlurHandler(this, - blurHandlerRegistration); - if (null != getState().errorMessage) { getWidget().setAriaInvalid(true); @@ -127,20 +115,6 @@ public class CheckBoxConnector extends AbstractFieldConnector implements } @Override - public void onFocus(FocusEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).focus(); - } - - @Override - public void onBlur(BlurEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).blur(); - } - - @Override public void onClick(ClickEvent event) { if (!isEnabled()) { return; diff --git a/client/src/com/vaadin/client/ui/nativebutton/NativeButtonConnector.java b/client/src/com/vaadin/client/ui/nativebutton/NativeButtonConnector.java index 2aae9beae6..65d4a1eb9b 100644 --- a/client/src/com/vaadin/client/ui/nativebutton/NativeButtonConnector.java +++ b/client/src/com/vaadin/client/ui/nativebutton/NativeButtonConnector.java @@ -15,30 +15,20 @@ */ package com.vaadin.client.ui.nativebutton; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; -import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; -import com.vaadin.client.EventHelper; import com.vaadin.client.VCaption; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; import com.vaadin.client.ui.Icon; import com.vaadin.client.ui.VNativeButton; -import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.button.ButtonServerRpc; import com.vaadin.shared.ui.button.NativeButtonState; import com.vaadin.ui.NativeButton; @Connect(NativeButton.class) -public class NativeButtonConnector extends AbstractComponentConnector implements - BlurHandler, FocusHandler { - - private HandlerRegistration focusHandlerRegistration; - private HandlerRegistration blurHandlerRegistration; +public class NativeButtonConnector extends AbstractComponentConnector { @Override public void init() { @@ -47,6 +37,8 @@ public class NativeButtonConnector extends AbstractComponentConnector implements getWidget().buttonRpcProxy = getRpcProxy(ButtonServerRpc.class); getWidget().client = getConnection(); getWidget().paintableId = getConnectorId(); + + ConnectorFocusAndBlurHandler.addHandlers(this); } @Override @@ -59,10 +51,6 @@ public class NativeButtonConnector extends AbstractComponentConnector implements super.onStateChanged(stateChangeEvent); getWidget().disableOnClick = getState().disableOnClick; - focusHandlerRegistration = EventHelper.updateFocusHandler(this, - focusHandlerRegistration); - blurHandlerRegistration = EventHelper.updateBlurHandler(this, - blurHandlerRegistration); // Set text VCaption.setCaptionText(getWidget(), getState()); @@ -107,19 +95,4 @@ public class NativeButtonConnector extends AbstractComponentConnector implements public NativeButtonState getState() { return (NativeButtonState) super.getState(); } - - @Override - public void onFocus(FocusEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).focus(); - } - - @Override - public void onBlur(BlurEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).blur(); - } - } diff --git a/client/src/com/vaadin/client/ui/nativeselect/NativeSelectConnector.java b/client/src/com/vaadin/client/ui/nativeselect/NativeSelectConnector.java index 938903da9a..d6ff2015b4 100644 --- a/client/src/com/vaadin/client/ui/nativeselect/NativeSelectConnector.java +++ b/client/src/com/vaadin/client/ui/nativeselect/NativeSelectConnector.java @@ -16,55 +16,23 @@ package com.vaadin.client.ui.nativeselect; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.vaadin.client.EventHelper; -import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; import com.vaadin.client.ui.VNativeSelect; import com.vaadin.client.ui.optiongroup.OptionGroupBaseConnector; -import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; import com.vaadin.shared.ui.Connect; import com.vaadin.ui.NativeSelect; @Connect(NativeSelect.class) -public class NativeSelectConnector extends OptionGroupBaseConnector implements - BlurHandler, FocusHandler { +public class NativeSelectConnector extends OptionGroupBaseConnector { - private HandlerRegistration focusHandlerRegistration = null; - private HandlerRegistration blurHandlerRegistration = null; - - public NativeSelectConnector() { - super(); - } - - @OnStateChange("registeredEventListeners") - private void onServerEventListenerChanged() { - focusHandlerRegistration = EventHelper.updateFocusHandler(this, - focusHandlerRegistration, getWidget().getSelect()); - blurHandlerRegistration = EventHelper.updateBlurHandler(this, - blurHandlerRegistration, getWidget().getSelect()); + @Override + protected void init() { + super.init(); + ConnectorFocusAndBlurHandler.addHandlers(this, getWidget().getSelect()); } @Override public VNativeSelect getWidget() { return (VNativeSelect) super.getWidget(); } - - @Override - public void onFocus(FocusEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).focus(); - } - - @Override - public void onBlur(BlurEvent event) { - // EventHelper.updateFocusHandler ensures that this is called only when - // there is a listener on server side - getRpcProxy(FocusAndBlurServerRpc.class).blur(); - } - } diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java index fc3e6ca0fc..23091d0ad5 100644 --- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java +++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java @@ -27,8 +27,8 @@ import com.vaadin.client.BrowserInfo; import com.vaadin.client.Paintable; import com.vaadin.client.TooltipInfo; import com.vaadin.client.UIDL; -import com.vaadin.client.WidgetUtil; import com.vaadin.client.VConsole; +import com.vaadin.client.WidgetUtil; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractComponentConnector; import com.vaadin.client.ui.VTree; @@ -62,6 +62,10 @@ public class TreeConnector extends AbstractComponentConnector implements if (uidl.hasAttribute("partialUpdate")) { handleUpdate(uidl); + + // IE8 needs a hack to measure the tree again after update + WidgetUtil.forceIE8Redraw(getWidget().getElement()); + getWidget().rendering = false; return; } diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index d67c953c6e..f5656dfdc4 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -59,6 +59,7 @@ import com.vaadin.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.client.ResourceLoader.ResourceLoadListener; import com.vaadin.client.ServerConnector; import com.vaadin.client.UIDL; +import com.vaadin.client.Util; import com.vaadin.client.VConsole; import com.vaadin.client.ValueMap; import com.vaadin.client.annotations.OnStateChange; @@ -71,6 +72,7 @@ import com.vaadin.client.ui.ShortcutActionHandler; import com.vaadin.client.ui.VNotification; import com.vaadin.client.ui.VOverlay; import com.vaadin.client.ui.VUI; +import com.vaadin.client.ui.VWindow; import com.vaadin.client.ui.layout.MayScrollChildren; import com.vaadin.client.ui.window.WindowConnector; import com.vaadin.server.Page.Styles; @@ -319,19 +321,19 @@ public class UIConnector extends AbstractSingleComponentContainerConnector Scheduler.get().scheduleDeferred(new Command() { @Override public void execute() { - ComponentConnector paintable = (ComponentConnector) uidl + ComponentConnector connector = (ComponentConnector) uidl .getPaintableAttribute("focused", getConnection()); - if (paintable == null) { + if (connector == null) { // Do not try to focus invisible components which not // present in UIDL return; } - final Widget toBeFocused = paintable.getWidget(); + final Widget toBeFocused = connector.getWidget(); /* * Two types of Widgets can be focused, either implementing - * GWT HasFocus of a thinner Vaadin specific Focusable + * GWT Focusable of a thinner Vaadin specific Focusable * interface. */ if (toBeFocused instanceof com.google.gwt.user.client.ui.Focusable) { @@ -340,7 +342,14 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } else if (toBeFocused instanceof Focusable) { ((Focusable) toBeFocused).focus(); } else { - VConsole.log("Could not focus component"); + getLogger() + .severe("Server is trying to set focus to the widget of connector " + + Util.getConnectorString(connector) + + " but it is not focusable. The widget should implement either " + + com.google.gwt.user.client.ui.Focusable.class + .getName() + + " or " + + Focusable.class.getName()); } } }); @@ -669,6 +678,19 @@ public class UIConnector extends AbstractSingleComponentContainerConnector if (c instanceof WindowConnector) { WindowConnector wc = (WindowConnector) c; wc.setWindowOrderAndPosition(); + VWindow window = wc.getWidget(); + if (!window.isAttached()) { + + // Attach so that all widgets inside the Window are attached + // when their onStateChange is run + + // Made invisible here for legacy reasons and made visible + // at the end of stateChange. This dance could probably be + // removed + window.setVisible(false); + window.show(); + } + } } @@ -752,8 +774,7 @@ public class UIConnector extends AbstractSingleComponentContainerConnector getState().pushConfiguration.mode.isEnabled()); } if (stateChangeEvent.hasPropertyChanged("reconnectDialogConfiguration")) { - getConnection().getConnectionStateHandler() - .configurationUpdated(); + getConnection().getConnectionStateHandler().configurationUpdated(); } if (stateChangeEvent.hasPropertyChanged("overlayContainerLabel")) { @@ -1022,59 +1043,16 @@ public class UIConnector extends AbstractSingleComponentContainerConnector } - forceStateChangeRecursively(UIConnector.this); - // UIDL has no stored URL which we can repaint so we do some find and - // replace magic... - String newThemeBase = getConnection().translateVaadinUri("theme://"); - replaceThemeAttribute(oldThemeBase, newThemeBase); + // Request a full resynchronization from the server to deal with legacy + // components + getConnection().getMessageSender().resynchronize(); + // Immediately update state and do layout while waiting for the resync + forceStateChangeRecursively(UIConnector.this); getLayoutManager().forceLayout(); } /** - * Finds all attributes where theme:// urls have possibly been used and - * replaces any old theme url with a new one - * - * @param oldPrefix - * The start of the old theme URL - * @param newPrefix - * The start of the new theme URL - */ - private void replaceThemeAttribute(String oldPrefix, String newPrefix) { - // Images - replaceThemeAttribute("src", oldPrefix, newPrefix); - // Embedded flash - replaceThemeAttribute("value", oldPrefix, newPrefix); - replaceThemeAttribute("movie", oldPrefix, newPrefix); - } - - /** - * Finds any attribute of the given type where theme:// urls have possibly - * been used and replaces any old theme url with a new one - * - * @param attributeName - * The name of the attribute, e.g. "src" - * @param oldPrefix - * The start of the old theme URL - * @param newPrefix - * The start of the new theme URL - */ - private void replaceThemeAttribute(String attributeName, String oldPrefix, - String newPrefix) { - // Find all "attributeName=" which start with "oldPrefix" using e.g. - // [^src='http://oldpath'] - NodeList<Element> elements = querySelectorAll("[" + attributeName - + "^='" + oldPrefix + "']"); - for (int i = 0; i < elements.getLength(); i++) { - Element element = elements.getItem(i); - element.setAttribute( - attributeName, - element.getAttribute(attributeName).replace(oldPrefix, - newPrefix)); - } - } - - /** * Force a full recursive recheck of every connector's state variables. * * @see #forceStateChange() diff --git a/client/src/com/vaadin/client/ui/window/WindowConnector.java b/client/src/com/vaadin/client/ui/window/WindowConnector.java index 9b710981d8..9ea3c8bb68 100644 --- a/client/src/com/vaadin/client/ui/window/WindowConnector.java +++ b/client/src/com/vaadin/client/ui/window/WindowConnector.java @@ -17,6 +17,8 @@ package com.vaadin.client.ui.window; import java.util.logging.Logger; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Node; @@ -354,10 +356,6 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector if (state.modal != window.vaadinModality) { window.setVaadinModality(!window.vaadinModality); } - if (!window.isAttached()) { - window.setVisible(false); // hide until possible centering - window.show(); - } boolean resizeable = state.resizable && state.windowMode == WindowMode.NORMAL; window.setResizable(resizeable); @@ -407,7 +405,13 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector window.centered = state.centered; // Ensure centering before setting visible (#16486) if (window.centered && getState().windowMode != WindowMode.MAXIMIZED) { - window.center(); + Scheduler.get().scheduleFinally(new ScheduledCommand() { + + @Override + public void execute() { + getWidget().center(); + } + }); } window.setVisible(true); diff --git a/client/src/com/vaadin/client/widget/grid/DefaultEditorEventHandler.java b/client/src/com/vaadin/client/widget/grid/DefaultEditorEventHandler.java new file mode 100644 index 0000000000..564b0f1a03 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/DefaultEditorEventHandler.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.client.widget.grid; + +import com.google.gwt.core.client.Duration; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.user.client.Event; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.FocusUtil; +import com.vaadin.client.widget.grid.events.EditorMoveEvent; +import com.vaadin.client.widget.grid.events.EditorOpenEvent; +import com.vaadin.client.widgets.Grid.Editor; +import com.vaadin.client.widgets.Grid.EditorDomEvent; + +/** + * The default handler for Grid editor events. Offers several overridable + * protected methods for easier customization. + * + * @since + * @author Vaadin Ltd + */ +public class DefaultEditorEventHandler<T> implements Editor.EventHandler<T> { + + public static final int KEYCODE_OPEN = KeyCodes.KEY_ENTER; + public static final int KEYCODE_MOVE = KeyCodes.KEY_ENTER; + public static final int KEYCODE_CLOSE = KeyCodes.KEY_ESCAPE; + + private double lastTouchEventTime = 0; + private int lastTouchEventX = -1; + private int lastTouchEventY = -1; + private int lastTouchEventRow = -1; + + /** + * Returns whether the given event is a touch event that should open the + * editor. + * + * @param event + * the received event + * @return whether the event is a touch open event + */ + protected boolean isTouchOpenEvent(EditorDomEvent<T> event) { + final Event e = event.getDomEvent(); + final int type = e.getTypeInt(); + + final double now = Duration.currentTimeMillis(); + final int currentX = WidgetUtil.getTouchOrMouseClientX(e); + final int currentY = WidgetUtil.getTouchOrMouseClientY(e); + + final boolean validTouchOpenEvent = type == Event.ONTOUCHEND + && now - lastTouchEventTime < 500 + && lastTouchEventRow == event.getCell().getRowIndex() + && Math.abs(lastTouchEventX - currentX) < 20 + && Math.abs(lastTouchEventY - currentY) < 20; + + if (type == Event.ONTOUCHSTART) { + lastTouchEventX = currentX; + lastTouchEventY = currentY; + } + + if (type == Event.ONTOUCHEND) { + lastTouchEventTime = now; + lastTouchEventRow = event.getCell().getRowIndex(); + } + + return validTouchOpenEvent; + } + + /** + * Returns whether the given event should open the editor. The default + * implementation returns true if and only if the event is a doubleclick or + * if it is a keydown event and the keycode is {@link #KEYCODE_OPEN}. + * + * @param event + * the received event + * @return true if the event is an open event, false otherwise + */ + protected boolean isOpenEvent(EditorDomEvent<T> event) { + final Event e = event.getDomEvent(); + return e.getTypeInt() == Event.ONDBLCLICK + || (e.getTypeInt() == Event.ONKEYDOWN && e.getKeyCode() == KEYCODE_OPEN) + || isTouchOpenEvent(event); + } + + /** + * Opens the editor on the appropriate row if the received event is an open + * event. The default implementation uses + * {@link #isOpenEvent(EditorDomEvent) isOpenEvent}. + * + * @param event + * the received event + * @return true if this method handled the event and nothing else should be + * done, false otherwise + */ + protected boolean handleOpenEvent(EditorDomEvent<T> event) { + if (isOpenEvent(event)) { + final EventCellReference<T> cell = event.getCell(); + + editRow(event, cell.getRowIndex(), cell.getColumnIndexDOM()); + + // FIXME should be in editRow + event.getGrid().fireEvent(new EditorOpenEvent(cell)); + + event.getDomEvent().preventDefault(); + + return true; + } + return false; + } + + /** + * Moves the editor to another row if the received event is a move event. + * The default implementation moves the editor to the clicked row if the + * event is a click; otherwise, if the event is a keydown and the keycode is + * {@link #KEYCODE_MOVE}, moves the editor one row up or down if the shift + * key is pressed or not, respectively. + * + * @param event + * the received event + * @return true if this method handled the event and nothing else should be + * done, false otherwise + */ + protected boolean handleMoveEvent(EditorDomEvent<T> event) { + Event e = event.getDomEvent(); + final EventCellReference<T> cell = event.getCell(); + + // TODO: Move on touch events + if (e.getTypeInt() == Event.ONCLICK) { + + editRow(event, cell.getRowIndex(), cell.getColumnIndexDOM()); + + // FIXME should be in editRow + event.getGrid().fireEvent(new EditorMoveEvent(cell)); + + return true; + } + + else if (e.getTypeInt() == Event.ONKEYDOWN + && e.getKeyCode() == KEYCODE_MOVE) { + + editRow(event, event.getRowIndex() + (e.getShiftKey() ? -1 : +1), + event.getFocusedColumnIndex()); + + // FIXME should be in editRow + event.getGrid().fireEvent(new EditorMoveEvent(cell)); + + return true; + } + + return false; + } + + /** + * Returns whether the given event should close the editor. The default + * implementation returns true if and only if the event is a keydown event + * and the keycode is {@link #KEYCODE_CLOSE}. + * + * @param event + * the received event + * @return true if the event is a close event, false otherwise + */ + protected boolean isCloseEvent(EditorDomEvent<T> event) { + final Event e = event.getDomEvent(); + return e.getTypeInt() == Event.ONKEYDOWN + && e.getKeyCode() == KEYCODE_CLOSE; + } + + /** + * Closes the editor if the received event is a close event. The default + * implementation uses {@link #isCloseEvent(EditorDomEvent) isCloseEvent}. + * + * @param event + * the received event + * @return true if this method handled the event and nothing else should be + * done, false otherwise + */ + protected boolean handleCloseEvent(EditorDomEvent<T> event) { + if (isCloseEvent(event)) { + event.getEditor().cancel(); + FocusUtil.setFocus(event.getGrid(), true); + return true; + } + return false; + } + + protected void editRow(EditorDomEvent<T> event, int rowIndex, int colIndex) { + int rowCount = event.getGrid().getDataSource().size(); + // Limit rowIndex between 0 and rowCount - 1 + rowIndex = Math.max(0, Math.min(rowCount - 1, rowIndex)); + + int colCount = event.getGrid().getVisibleColumns().size(); + // Limit colIndex between 0 and colCount - 1 + colIndex = Math.max(0, Math.min(colCount - 1, colIndex)); + + event.getEditor().editRow(rowIndex, colIndex); + } + + @Override + public boolean handleEvent(EditorDomEvent<T> event) { + final Editor<T> editor = event.getEditor(); + final boolean isBody = event.getCell().isBody(); + + if (event.getGrid().isEditorActive()) { + return (!editor.isBuffered() && isBody && handleMoveEvent(event)) + || handleCloseEvent(event) + // Swallow events if editor is open and buffered (modal) + || editor.isBuffered(); + } else { + return event.getGrid().isEnabled() && isBody + && handleOpenEvent(event); + } + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/widget/grid/events/EditorCloseEvent.java b/client/src/com/vaadin/client/widget/grid/events/EditorCloseEvent.java new file mode 100644 index 0000000000..99f59aa82a --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/EditorCloseEvent.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.widget.grid.events; + +import com.vaadin.client.widget.grid.CellReference; + +/** + * Event that gets fired when an open editor is closed (and not reopened + * elsewhere) + */ +public class EditorCloseEvent extends EditorEvent { + + public EditorCloseEvent(CellReference<?> cell) { + super(cell); + } + + @Override + protected void dispatch(EditorEventHandler handler) { + handler.onEditorClose(this); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/widget/grid/events/EditorEvent.java b/client/src/com/vaadin/client/widget/grid/events/EditorEvent.java new file mode 100644 index 0000000000..eb34033197 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/EditorEvent.java @@ -0,0 +1,108 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.client.widget.grid.CellReference; +import com.vaadin.client.widgets.Grid; +import com.vaadin.client.widgets.Grid.Column; + +/** + * Base class for editor events. + */ +public abstract class EditorEvent extends GwtEvent<EditorEventHandler> { + public static final Type<EditorEventHandler> TYPE = new Type<EditorEventHandler>(); + + private CellReference<?> cell; + + protected EditorEvent(CellReference<?> cell) { + this.cell = cell; + } + + @Override + public Type<EditorEventHandler> getAssociatedType() { + return TYPE; + } + + /** + * Get a reference to the Grid that fired this Event. + * + * @return a Grid reference + */ + @SuppressWarnings("unchecked") + public <T> Grid<T> getGrid() { + return (Grid<T>) cell.getGrid(); + } + + /** + * Get a reference to the cell that was active when this Event was fired. + * NOTE: do <i>NOT</i> rely on this information remaining accurate after + * leaving the event handler. + * + * @return a cell reference + */ + @SuppressWarnings("unchecked") + public <T> CellReference<T> getCell() { + return (CellReference<T>) cell; + } + + /** + * Get a reference to the row that was active when this Event was fired. + * NOTE: do <i>NOT</i> rely on this information remaining accurate after + * leaving the event handler. + * + * @return a row data object + */ + @SuppressWarnings("unchecked") + public <T> T getRow() { + return (T) cell.getRow(); + } + + /** + * Get the index of the row that was active when this Event was fired. NOTE: + * do <i>NOT</i> rely on this information remaining accurate after leaving + * the event handler. + * + * @return an integer value + */ + public int getRowIndex() { + return cell.getRowIndex(); + } + + /** + * Get a reference to the column that was active when this Event was fired. + * NOTE: do <i>NOT</i> rely on this information remaining accurate after + * leaving the event handler. + * + * @return a column object + */ + @SuppressWarnings("unchecked") + public <C, T> Column<C, T> getColumn() { + return (Column<C, T>) cell.getColumn(); + } + + /** + * Get the index of the column that was active when this Event was fired. + * NOTE: do <i>NOT</i> rely on this information remaining accurate after + * leaving the event handler. + * + * @return an integer value + */ + public int getColumnIndex() { + return cell.getColumnIndex(); + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/widget/grid/events/EditorEventHandler.java b/client/src/com/vaadin/client/widget/grid/events/EditorEventHandler.java new file mode 100644 index 0000000000..4f9396a9f1 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/EditorEventHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Common handler interface for editor events + */ +public interface EditorEventHandler extends EventHandler { + + /** + * Action to perform when the editor has been opened + * + * @param e + * an editor open event object + */ + public void onEditorOpen(EditorOpenEvent e); + + /** + * Action to perform when the editor is re-opened on another row + * + * @param e + * an editor move event object + */ + public void onEditorMove(EditorMoveEvent e); + + /** + * Action to perform when the editor is closed + * + * @param e + * an editor close event object + */ + public void onEditorClose(EditorCloseEvent e); + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/widget/grid/events/EditorMoveEvent.java b/client/src/com/vaadin/client/widget/grid/events/EditorMoveEvent.java new file mode 100644 index 0000000000..0e5e2dcd7b --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/EditorMoveEvent.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.widget.grid.events; + +import com.vaadin.client.widget.grid.CellReference; + +/** + * Event that gets fired when an already open editor is closed and re-opened on + * another row + */ +public class EditorMoveEvent extends EditorEvent { + + public EditorMoveEvent(CellReference<?> cell) { + super(cell); + } + + @Override + protected void dispatch(EditorEventHandler handler) { + handler.onEditorMove(this); + } +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/widget/grid/events/EditorOpenEvent.java b/client/src/com/vaadin/client/widget/grid/events/EditorOpenEvent.java new file mode 100644 index 0000000000..df0171945f --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/EditorOpenEvent.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.widget.grid.events; + +import com.vaadin.client.widget.grid.CellReference; + +/** + * Event that gets fired when the editor is opened + */ +public class EditorOpenEvent extends EditorEvent { + + public EditorOpenEvent(CellReference<?> cell) { + super(cell); + } + + @Override + protected void dispatch(EditorEventHandler handler) { + handler.onEditorOpen(this); + } + +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java b/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java index 1e47d3ad6b..c64908f24c 100644 --- a/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java +++ b/client/src/com/vaadin/client/widget/grid/selection/MultiSelectionRenderer.java @@ -56,6 +56,8 @@ import com.vaadin.client.widgets.Grid; public class MultiSelectionRenderer<T> extends ClickableRenderer<Boolean, CheckBox> { + private static final String SELECTION_CHECKBOX_CLASSNAME = "-selection-checkbox"; + /** The size of the autoscroll area, both top and bottom. */ private static final int SCROLL_AREA_GRADIENT_PX = 100; @@ -591,6 +593,8 @@ public class MultiSelectionRenderer<T> extends @Override public CheckBox createWidget() { final CheckBox checkBox = GWT.create(CheckBox.class); + checkBox.setStylePrimaryName(grid.getStylePrimaryName() + + SELECTION_CHECKBOX_CLASSNAME); CheckBoxEventHandler handler = new CheckBoxEventHandler(checkBox); // Sink events diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 436b512294..43eeb7a0ce 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -29,11 +29,13 @@ import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; +import com.google.gwt.animation.client.Animation; 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.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.DivElement; @@ -48,6 +50,7 @@ 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.dom.client.Touch; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.Command; @@ -72,6 +75,7 @@ import com.vaadin.client.widget.escalator.PositionFunction.AbsolutePosition; import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition; import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition; import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition; +import com.vaadin.client.widget.escalator.Row; import com.vaadin.client.widget.escalator.RowContainer; import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent; @@ -302,8 +306,6 @@ public class Escalator extends Widget implements RequiresResize, static class JsniUtil { public static class TouchHandlerBundle { - private static final double FLICK_POLL_FREQUENCY = 100d; - /** * A <a href= * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html" @@ -338,105 +340,8 @@ public class Escalator extends Widget implements RequiresResize, }-*/; } - private double touches = 0; - private int lastX = 0; - private int lastY = 0; - private boolean snappedScrollEnabled = true; - private double deltaX = 0; - private double deltaY = 0; - private final Escalator escalator; - private CustomTouchEvent latestTouchMoveEvent; - - /** The timestamp of {@link #flickPageX1} and {@link #flickPageY1} */ - private double flickStartTime = 0; - - /** The timestamp of {@link #flickPageX2} and {@link #flickPageY2} */ - private double flickTimestamp = 0; - - /** The most recent flick touch reference Y */ - private double flickPageY1 = -1; - /** The most recent flick touch reference X */ - private double flickPageX1 = -1; - - /** The previous flick touch reference Y, before {@link #flickPageY1} */ - private double flickPageY2 = -1; - /** The previous flick touch reference X, before {@link #flickPageX1} */ - private double flickPageX2 = -1; - - /** - * This animation callback guarantees the fact that we don't scroll - * the grid more than once per visible frame. - * - * It seems that there will never be more touch events than there - * are rendered frames, but there's no guarantee for that. If it was - * guaranteed, we probably could do all of this immediately in - * {@link #touchMove(CustomTouchEvent)}, instead of deferring it - * over here. - */ - private AnimationCallback mover = new AnimationCallback() { - - @Override - public void execute(double timestamp) { - if (touches != 1) { - return; - } - - final int x = latestTouchMoveEvent.getPageX(); - final int y = latestTouchMoveEvent.getPageY(); - - /* - * Check if we need a new flick coordinate sample ( more - * than FLICK_POLL_FREQUENCY ms have passed since the last - * sample ) - */ - if (System.currentTimeMillis() - flickTimestamp > FLICK_POLL_FREQUENCY) { - - flickTimestamp = System.currentTimeMillis(); - // Set target coordinates - flickPageY2 = y; - flickPageX2 = x; - } - - deltaX = x - lastX; - deltaY = y - lastY; - lastX = x; - lastY = y; - - // 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; } @@ -468,79 +373,157 @@ public class Escalator extends Widget implements RequiresResize, }); }-*/; - public void touchStart(final CustomTouchEvent event) { - touches = event.getNativeEvent().getTouches().length(); - if (touches != 1) { - return; + // Duration of the inertial scrolling simulation. Devices with + // larger screens take longer durations. + private static final int DURATION = (int)Window.getClientHeight(); + // multiply scroll velocity with repeated touching + private int acceleration = 1; + private boolean touching = false; + // Two movement objects for storing status and processing touches + private Movement yMov, xMov; + final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7, F_AXIS = 1; + + // The object to deal with one direction scrolling + private class Movement { + final List<Double> speeds = new ArrayList<Double>(); + final ScrollbarBundle scroll; + double position, offset, velocity, prevPos, prevTime, delta; + boolean run, vertical; + + public Movement(boolean vertical) { + this.vertical = vertical; + scroll = vertical ? escalator.verticalScrollbar : escalator.horizontalScrollbar; } - escalator.scroller.cancelFlickScroll(); + public void startTouch(CustomTouchEvent event) { + speeds.clear(); + prevPos = pagePosition(event); + prevTime = Duration.currentTimeMillis(); + } + public void moveTouch(CustomTouchEvent event) { + double pagePosition = pagePosition(event); + if (pagePosition > -1) { + delta = prevPos - pagePosition; + double now = Duration.currentTimeMillis(); + double ellapsed = now - prevTime; + velocity = delta / ellapsed; + // if last speed was so low, reset speeds and start storing again + if (speeds.size() > 0 && !validSpeed(speeds.get(0))) { + speeds.clear(); + run = true; + } + speeds.add(0, velocity); + prevTime = now; + prevPos = pagePosition; + } + } + public void endTouch(CustomTouchEvent event) { + // Compute average speed + velocity = 0; + for (double s : speeds) { + velocity += s / speeds.size(); + } + position = scroll.getScrollPos(); + // Compute offset, and adjust it with an easing curve so as movement is smoother. + offset = F_VEL * velocity * acceleration * easingInOutCos(velocity, MAX_VEL); + // Check that offset does not over-scroll + double minOff = -scroll.getScrollPos(); + double maxOff = scroll.getScrollSize() - scroll.getOffsetSize() + minOff; + offset = Math.min(Math.max(offset, minOff), maxOff); + // Enable or disable inertia movement in this axis + run = validSpeed(velocity) && minOff < 0 && maxOff > 0; + if (run) { + event.getNativeEvent().preventDefault(); + } + } + void validate(Movement other) { + if (!run || other.velocity > 0 && Math.abs(velocity / other.velocity) < F_AXIS) { + delta = offset = 0; + run = false; + } + } + void stepAnimation(double progress) { + scroll.setScrollPos(position + offset * progress); + } - lastX = event.getPageX(); - lastY = event.getPageY(); + int pagePosition(CustomTouchEvent event) { + JsArray<Touch> a = event.getNativeEvent().getTouches(); + return vertical ? a.get(0).getPageY() : a.get(0).getPageX(); + } + boolean validSpeed(double speed) { + return Math.abs(speed) > MIN_VEL; + } + } - // Reset flick parameters - flickPageX1 = lastX; - flickPageX2 = -1; - flickPageY1 = lastY; - flickPageY2 = -1; - flickStartTime = System.currentTimeMillis(); - flickTimestamp = 0; + // Using GWT animations which take care of native animation frames. + private Animation animation = new Animation() { + public void onUpdate(double progress) { + xMov.stepAnimation(progress); + yMov.stepAnimation(progress); + } + public double interpolate(double progress) { + return easingOutCirc(progress); + }; + public void onComplete() { + touching = false; + escalator.body.domSorter.reschedule(); + }; + public void run(int duration) { + if (xMov.run || yMov.run) { + super.run(duration); + } else { + onComplete(); + } + }; + }; - snappedScrollEnabled = true; + public void touchStart(final CustomTouchEvent event) { + if (event.getNativeEvent().getTouches().length() == 1) { + if (yMov == null) { + yMov = new Movement(true); + xMov = new Movement(false); + } + if (animation.isRunning()) { + acceleration += F_ACC; + event.getNativeEvent().preventDefault(); + animation.cancel(); + } else { + acceleration = 1; + } + xMov.startTouch(event); + yMov.startTouch(event); + touching = 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(); + xMov.moveTouch(event); + yMov.moveTouch(event); + xMov.validate(yMov); + yMov.validate(xMov); + moveScrollFromEvent(escalator, xMov.delta, yMov.delta, event.getNativeEvent()); } public void touchEnd(final CustomTouchEvent event) { - touches = event.getNativeEvent().getTouches().length(); - - if (touches == 0) { - - /* - * We want to smooth the flick calculations here. We have - * taken a frame of reference every FLICK_POLL_FREQUENCY. - * But if the sample is too fresh, we might introduce noise - * in our sampling, so we use the older sample instead. it - * might be less accurate, but it's smoother. - * - * flickPage?1 is the most recent one, while flickPage?2 is - * the previous one. - */ - - final double finalPageY; - final double finalPageX; - double deltaT = flickTimestamp - flickStartTime; - boolean onlyOneSample = flickPageX2 < 0 || flickPageY2 < 0; - if (onlyOneSample) { - finalPageX = latestTouchMoveEvent.getPageX(); - finalPageY = latestTouchMoveEvent.getPageY(); - } else { - finalPageY = flickPageY2; - finalPageX = flickPageX2; - } - - double deltaX = finalPageX - flickPageX1; - double deltaY = finalPageY - flickPageY1; + xMov.endTouch(event); + yMov.endTouch(event); + xMov.validate(yMov); + yMov.validate(xMov); + // Adjust duration so as longer movements take more duration + boolean vert = !xMov.run || yMov.run && Math.abs(yMov.offset) > Math.abs(xMov.offset); + double delta = Math.abs((vert ? yMov : xMov).offset); + animation.run((int)(3 * DURATION * easingOutExp(delta))); + } - escalator.scroller - .handleFlickScroll(deltaX, deltaY, deltaT); - escalator.body.domSorter.reschedule(); - } + private double easingInOutCos(double val, double max) { + return 0.5 - 0.5 * Math.cos(Math.PI * Math.signum(val) + * Math.min(Math.abs(val), max) / max); + } + private double easingOutExp(double delta) { + return (1 - Math.pow(2, -delta / 1000)); + } + private double easingOutCirc(double progress) { + return Math.sqrt(1 - (progress - 1) * (progress - 1)); } } @@ -570,117 +553,6 @@ public class Escalator extends Widget implements RequiresResize, } } - /** - * The animation callback that handles the animation of a touch-scrolling - * flick with inertia. - */ - private class FlickScrollAnimator implements AnimationCallback { - private static final double MIN_MAGNITUDE = 0.005; - private static final double MAX_SPEED = 7; - - private double velX; - private double velY; - private double prevTime = 0; - private int millisLeft; - private double xFric; - private double yFric; - - private boolean cancelled = false; - private double lastLeft; - private double lastTop; - - /** - * Creates a new animation callback to handle touch-scrolling flick with - * inertia. - * - * @param deltaX - * the last scrolling delta in the x-axis in a touchmove - * @param deltaY - * the last scrolling delta in the y-axis in a touchmove - * @param lastTime - * the timestamp of the last touchmove - */ - public FlickScrollAnimator(final double deltaX, final double deltaY, - final double deltaT) { - velX = Math.max(Math.min(deltaX / deltaT, MAX_SPEED), -MAX_SPEED); - velY = Math.max(Math.min(deltaY / deltaT, MAX_SPEED), -MAX_SPEED); - - lastLeft = horizontalScrollbar.getScrollPos(); - lastTop = verticalScrollbar.getScrollPos(); - - /* - * If we're scrolling mainly in one of the four major directions, - * and only a teeny bit to any other side, snap the scroll to that - * major direction instead. - */ - final double[] snapDeltas = Escalator.snapDeltas(velX, velY, - RATIO_OF_30_DEGREES); - velX = snapDeltas[0]; - velY = snapDeltas[1]; - - if (velX * velX + velY * velY > MIN_MAGNITUDE) { - millisLeft = 1500; - xFric = velX / millisLeft; - yFric = velY / millisLeft; - } else { - millisLeft = 0; - } - - } - - @Override - public void execute(final double doNotUseThisTimestamp) { - /* - * We cannot use the timestamp provided to this method since it is - * of a format that cannot be determined at will. Therefore, we need - * a timestamp format that we can handle, so our calculations are - * correct. - */ - - if (millisLeft <= 0 || cancelled) { - scroller.currentFlickScroller = null; - return; - } - - final double timestamp = Duration.currentTimeMillis(); - if (prevTime == 0) { - prevTime = timestamp; - AnimationScheduler.get().requestAnimationFrame(this); - return; - } - - double currentLeft = horizontalScrollbar.getScrollPos(); - double currentTop = verticalScrollbar.getScrollPos(); - - final double timeDiff = timestamp - prevTime; - double left = currentLeft - velX * timeDiff; - setScrollLeft(left); - velX -= xFric * timeDiff; - - double top = currentTop - velY * timeDiff; - setScrollTop(top); - velY -= yFric * timeDiff; - - cancelBecauseOfEdgeOrCornerMaybe(); - - prevTime = timestamp; - millisLeft -= timeDiff; - lastLeft = currentLeft; - lastTop = currentTop; - AnimationScheduler.get().requestAnimationFrame(this); - } - - private void cancelBecauseOfEdgeOrCornerMaybe() { - if (lastLeft == horizontalScrollbar.getScrollPos() - && lastTop == verticalScrollbar.getScrollPos()) { - cancel(); - } - } - - public void cancel() { - cancelled = true; - } - } /** * ScrollDestination case-specific handling logic. @@ -760,11 +632,6 @@ public class Escalator extends Widget implements RequiresResize, 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); @@ -782,7 +649,7 @@ public class Escalator extends Widget implements RequiresResize, 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. @@ -1100,30 +967,6 @@ public class Escalator extends Widget implements RequiresResize, } }-*/; - private void cancelFlickScroll() { - if (currentFlickScroller != null) { - currentFlickScroller.cancel(); - } - } - - /** - * Handles a touch-based flick scroll. - * - * @param deltaX - * the last scrolling delta in the x-axis in a touchmove - * @param deltaY - * the last scrolling delta in the y-axis in a touchmove - * @param lastTime - * the timestamp of the last touchmove - */ - public void handleFlickScroll(double deltaX, double deltaY, - double deltaT) { - currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY, - deltaT); - AnimationScheduler.get() - .requestAnimationFrame(currentFlickScroller); - } - public void scrollToColumn(final int columnIndex, final ScrollDestination destination, final int padding) { assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column"; @@ -1475,6 +1318,9 @@ public class Escalator extends Widget implements RequiresResize, cellElem.addClassName("frozen"); position.set(cellElem, scroller.lastScrollLeft, 0); } + if (columnConfiguration.frozenColumns > 0 && col == columnConfiguration.frozenColumns - 1) { + cellElem.addClassName("last-frozen"); + } } referenceRow = paintInsertRow(referenceRow, tr, row); @@ -1732,6 +1578,14 @@ public class Escalator extends Widget implements RequiresResize, } public void setColumnFrozen(int column, boolean frozen) { + toggleFrozenColumnClass(column, frozen, "frozen"); + + if (frozen) { + updateFreezePosition(column, scroller.lastScrollLeft); + } + } + + private void toggleFrozenColumnClass(int column, boolean frozen, String className) { final NodeList<TableRowElement> childRows = root.getRows(); for (int row = 0; row < childRows.getLength(); row++) { @@ -1742,16 +1596,16 @@ public class Escalator extends Widget implements RequiresResize, TableCellElement cell = tr.getCells().getItem(column); if (frozen) { - cell.addClassName("frozen"); + cell.addClassName(className); } else { - cell.removeClassName("frozen"); + cell.removeClassName(className); position.reset(cell); } } + } - if (frozen) { - updateFreezePosition(column, scroller.lastScrollLeft); - } + public void setColumnLastFrozen(int column, boolean lastFrozen) { + toggleFrozenColumnClass(column, lastFrozen, "last-frozen"); } public void updateFreezePosition(int column, double scrollLeft) { @@ -2470,9 +2324,9 @@ public class Escalator extends Widget implements RequiresResize, private boolean sortIfConditionsMet() { boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED; boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS; - boolean notAnimatingFlick = (scroller.currentFlickScroller == null); + boolean notTouchActivity = !scroller.touchHandlerBundle.touching; boolean conditionsMet = enoughFramesHavePassed - && enoughTimeHasPassed && notAnimatingFlick; + && enoughTimeHasPassed && notTouchActivity; if (conditionsMet) { resetConditions(); @@ -2652,15 +2506,7 @@ public class Escalator extends Widget implements RequiresResize, 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(); - } + domSorter.reschedule(); } } @@ -2756,17 +2602,33 @@ public class Escalator extends Widget implements RequiresResize, // move the surrounding rows to their correct places. double rowTop = (unupdatedLogicalStart + (end - start)) * getDefaultRowHeight(); - final ListIterator<TableRowElement> i = visualRowOrder - .listIterator(visualTargetIndex + (end - start)); - - int logicalRowIndexCursor = unupdatedLogicalStart; - while (i.hasNext()) { - rowTop += spacerContainer - .getSpacerHeight(logicalRowIndexCursor++); - final TableRowElement tr = i.next(); - setRowPosition(tr, 0, rowTop); - rowTop += getDefaultRowHeight(); + // TODO: Get rid of this try/catch block by fixing the + // underlying issue. The reason for this erroneous behavior + // might be that Escalator actually works 'by mistake', and + // the order of operations is, in fact, wrong. + try { + final ListIterator<TableRowElement> i = visualRowOrder + .listIterator(visualTargetIndex + (end - start)); + + int logicalRowIndexCursor = unupdatedLogicalStart; + while (i.hasNext()) { + rowTop += spacerContainer + .getSpacerHeight(logicalRowIndexCursor++); + + final TableRowElement tr = i.next(); + setRowPosition(tr, 0, rowTop); + rowTop += getDefaultRowHeight(); + } + } catch (Exception e) { + Logger logger = getLogger(); + logger.warning("Ignored out-of-bounds row element access"); + logger.warning("Escalator state: start=" + start + + ", end=" + end + ", visualTargetIndex=" + + visualTargetIndex + + ", visualRowOrder.size()=" + + visualRowOrder.size()); + logger.warning(e.toString()); } } @@ -4309,6 +4171,17 @@ public class Escalator extends Widget implements RequiresResize, firstUnaffectedCol = oldCount; } + if (oldCount > 0) { + header.setColumnLastFrozen(oldCount - 1, false); + body.setColumnLastFrozen(oldCount - 1, false); + footer.setColumnLastFrozen(oldCount - 1, false); + } + if (count > 0) { + header.setColumnLastFrozen(count - 1, true); + body.setColumnLastFrozen(count - 1, true); + footer.setColumnLastFrozen(count - 1, true); + } + for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) { header.setColumnFrozen(col, frozen); body.setColumnFrozen(col, frozen); @@ -5619,14 +5492,29 @@ public class Escalator extends Widget implements RequiresResize, horizontalScrollbar.setScrollbarThickness(scrollbarThickness); horizontalScrollbar .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() { + + private boolean queued = false; + @Override public void visibilityChanged( ScrollbarBundle.VisibilityChangeEvent event) { + if (queued) { + return; + } + queued = true; + /* * We either lost or gained a scrollbar. In any case, we * need to change the height, if it's defined by rows. */ - applyHeightByRows(); + Scheduler.get().scheduleFinally(new ScheduledCommand() { + + @Override + public void execute() { + applyHeightByRows(); + queued = false; + } + }); } }); @@ -6022,10 +5910,14 @@ public class Escalator extends Widget implements RequiresResize, public void scrollToRow(final int rowIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - validateScrollDestination(destination, padding); - verifyValidRowIndex(rowIndex); - - scroller.scrollToRow(rowIndex, destination, padding); + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + validateScrollDestination(destination, padding); + verifyValidRowIndex(rowIndex); + scroller.scrollToRow(rowIndex, destination, padding); + } + }); } private void verifyValidRowIndex(final int rowIndex) { @@ -6086,55 +5978,62 @@ public class Escalator extends Widget implements RequiresResize, * {@code destination == null}; or if {@code rowIndex == -1} and * there is no spacer open at that index. */ - public void scrollToRowAndSpacer(int rowIndex, - ScrollDestination destination, int padding) + public void scrollToRowAndSpacer(final int rowIndex, + final ScrollDestination destination, final int padding) throws IllegalArgumentException { - validateScrollDestination(destination, padding); - if (rowIndex != -1) { - verifyValidRowIndex(rowIndex); - } + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + validateScrollDestination(destination, padding); + if (rowIndex != -1) { + verifyValidRowIndex(rowIndex); + } - // row range - final Range rowRange; - if (rowIndex != -1) { - int rowTop = (int) Math.floor(body.getRowTop(rowIndex)); - int rowHeight = (int) Math.ceil(body.getDefaultRowHeight()); - rowRange = Range.withLength(rowTop, rowHeight); - } else { - rowRange = Range.withLength(0, 0); - } + // row range + final Range rowRange; + if (rowIndex != -1) { + int rowTop = (int) Math.floor(body.getRowTop(rowIndex)); + int rowHeight = (int) Math.ceil(body.getDefaultRowHeight()); + rowRange = Range.withLength(rowTop, rowHeight); + } else { + rowRange = Range.withLength(0, 0); + } - // get spacer - final SpacerContainer.SpacerImpl spacer = body.spacerContainer - .getSpacer(rowIndex); + // get spacer + final SpacerContainer.SpacerImpl spacer = body.spacerContainer + .getSpacer(rowIndex); - if (rowIndex == -1 && spacer == null) { - throw new IllegalArgumentException("Cannot scroll to row index " - + "-1, as there is no spacer open at that index."); - } + if (rowIndex == -1 && spacer == null) { + throw new IllegalArgumentException( + "Cannot scroll to row index " + + "-1, as there is no spacer open at that index."); + } - // make into target range - final Range targetRange; - if (spacer != null) { - final int spacerTop = (int) Math.floor(spacer.getTop()); - final int spacerHeight = (int) Math.ceil(spacer.getHeight()); - Range spacerRange = Range.withLength(spacerTop, spacerHeight); + // make into target range + final Range targetRange; + if (spacer != null) { + final int spacerTop = (int) Math.floor(spacer.getTop()); + final int spacerHeight = (int) Math.ceil(spacer.getHeight()); + Range spacerRange = Range.withLength(spacerTop, + spacerHeight); - targetRange = rowRange.combineWith(spacerRange); - } else { - targetRange = rowRange; - } + targetRange = rowRange.combineWith(spacerRange); + } else { + targetRange = rowRange; + } - // get params - int targetStart = targetRange.getStart(); - int targetEnd = targetRange.getEnd(); - double viewportStart = getScrollTop(); - double viewportEnd = viewportStart + body.getHeightOfSection(); + // get params + int targetStart = targetRange.getStart(); + int targetEnd = targetRange.getEnd(); + double viewportStart = getScrollTop(); + double viewportEnd = viewportStart + body.getHeightOfSection(); - double scrollPos = getScrollPos(destination, targetStart, targetEnd, - viewportStart, viewportEnd, padding); + double scrollPos = getScrollPos(destination, targetStart, + targetEnd, viewportStart, viewportEnd, padding); - setScrollTop(scrollPos); + setScrollTop(scrollPos); + } + }); } private static void validateScrollDestination( diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 58fc532a77..fc8151272d 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -31,7 +31,6 @@ import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; -import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.core.shared.GWT; @@ -42,6 +41,7 @@ import com.google.gwt.dom.client.EventTarget; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; import com.google.gwt.dom.client.TableRowElement; @@ -49,6 +49,9 @@ import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasFocusHandlers; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; @@ -79,6 +82,7 @@ import com.vaadin.client.Focusable; import com.vaadin.client.WidgetUtil; import com.vaadin.client.data.DataChangeHandler; import com.vaadin.client.data.DataSource; +import com.vaadin.client.data.DataSource.RowHandle; import com.vaadin.client.renderers.ComplexRenderer; import com.vaadin.client.renderers.Renderer; import com.vaadin.client.renderers.WidgetRenderer; @@ -103,6 +107,7 @@ import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; import com.vaadin.client.widget.grid.DataAvailableEvent; import com.vaadin.client.widget.grid.DataAvailableHandler; +import com.vaadin.client.widget.grid.DefaultEditorEventHandler; import com.vaadin.client.widget.grid.DetailsGenerator; import com.vaadin.client.widget.grid.EditorHandler; import com.vaadin.client.widget.grid.EditorHandler.EditorRequest; @@ -121,6 +126,9 @@ import com.vaadin.client.widget.grid.events.ColumnReorderEvent; import com.vaadin.client.widget.grid.events.ColumnReorderHandler; import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent; import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler; +import com.vaadin.client.widget.grid.events.EditorCloseEvent; +import com.vaadin.client.widget.grid.events.EditorEvent; +import com.vaadin.client.widget.grid.events.EditorEventHandler; import com.vaadin.client.widget.grid.events.FooterClickHandler; import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; @@ -206,8 +214,10 @@ import com.vaadin.shared.util.SharedUtil; * @author Vaadin Ltd */ public class Grid<T> extends ResizeComposite implements - HasSelectionHandlers<T>, SubPartAware, DeferredWorker, HasWidgets, - HasEnabled { + HasSelectionHandlers<T>, SubPartAware, DeferredWorker, Focusable, + com.google.gwt.user.client.ui.Focusable, HasWidgets, HasEnabled { + + private static final String SELECT_ALL_CHECKBOX_CLASSNAME = "-select-all-checkbox"; /** * Enum describing different sections of Grid. @@ -1081,14 +1091,10 @@ public class Grid<T> extends ResizeComposite implements } completed = true; - grid.getEditor().setErrorMessage(errorMessage); - - grid.getEditor().clearEditorColumnErrors(); - if (errorColumns != null) { - for (Column<?, T> column : errorColumns) { - grid.getEditor().setEditorColumnError(column, true); - } + if (errorColumns == null) { + errorColumns = Collections.emptySet(); } + grid.getEditor().setEditorError(errorMessage, errorColumns); } @Override @@ -1115,10 +1121,104 @@ public class Grid<T> extends ResizeComposite implements } /** + * A wrapper for native DOM events originating from Grid. In addition to the + * native event, contains a {@link CellReference} instance specifying which + * cell the event originated from. + * + * @since + * @param <T> + * The row type of the grid + */ + public static class GridEvent<T> { + private Event event; + private EventCellReference<T> cell; + + protected GridEvent(Event event, EventCellReference<T> cell) { + this.event = event; + this.cell = cell; + } + + /** + * Returns the wrapped DOM event. + * + * @return the DOM event + */ + public Event getDomEvent() { + return event; + } + + /** + * Returns the Grid cell this event originated from. + * + * @return the event cell + */ + public EventCellReference<T> getCell() { + return cell; + } + + /** + * Returns the Grid instance this event originated from. + * + * @return the grid + */ + public Grid<T> getGrid() { + return cell.getGrid(); + } + } + + /** + * A wrapper for native DOM events related to the {@link Editor Grid editor} + * . + * + * @since + * @param <T> + * the row type of the grid + */ + public static class EditorDomEvent<T> extends GridEvent<T> { + + protected EditorDomEvent(Event event, EventCellReference<T> cell) { + super(event, cell); + } + + /** + * Returns the editor of the Grid this event originated from. + * + * @return the related editor instance + */ + public Editor<T> getEditor() { + return getGrid().getEditor(); + } + + /** + * Returns the row index the editor is open at. If the editor is not + * open, returns -1. + * + * @return the index of the edited row or -1 if editor is not open + */ + public int getRowIndex() { + return getEditor().rowIndex; + } + + /** + * Returns the column index the editor was opened at. If the editor is + * not open, returns -1. + * + * @return the column index or -1 if editor is not open + */ + public int getFocusedColumnIndex() { + return getEditor().focusedColumnIndex; + } + } + + /** * An editor UI for Grid rows. A single Grid row at a time can be opened for * editing. + * + * @since + * @param <T> + * the row type of the grid */ - protected static class Editor<T> { + public static class Editor<T> { public static final int KEYCODE_SHOW = KeyCodes.KEY_ENTER; public static final int KEYCODE_HIDE = KeyCodes.KEY_ESCAPE; @@ -1126,15 +1226,41 @@ public class Grid<T> extends ResizeComposite implements private static final String ERROR_CLASS_NAME = "error"; private static final String NOT_EDITABLE_CLASS_NAME = "not-editable"; + /** + * A handler for events related to the Grid editor. Responsible for + * opening, moving or closing the editor based on the received event. + * + * @since + * @author Vaadin Ltd + * @param <T> + * the row type of the grid + */ + public interface EventHandler<T> { + /** + * Handles editor-related events in an appropriate way. Opens, + * moves, or closes the editor based on the given event. + * + * @param event + * the received event + * @return true if the event was handled and nothing else should be + * done, false otherwise + */ + boolean handleEvent(EditorDomEvent<T> event); + } + protected enum State { INACTIVE, ACTIVATING, BINDING, ACTIVE, SAVING } private Grid<T> grid; private EditorHandler<T> handler; + private EventHandler<T> eventHandler = GWT + .create(DefaultEditorEventHandler.class); private DivElement editorOverlay = DivElement.as(DOM.createDiv()); private DivElement cellWrapper = DivElement.as(DOM.createDiv()); + private DivElement frozenCellWrapper = DivElement.as(DOM.createDiv()); + private DivElement messageAndButtonsWrapper = DivElement.as(DOM .createDiv()); @@ -1146,15 +1272,25 @@ public class Grid<T> extends ResizeComposite implements private DivElement message = DivElement.as(DOM.createDiv()); private Map<Column<?, T>, Widget> columnToWidget = new HashMap<Column<?, T>, Widget>(); + private List<HandlerRegistration> focusHandlers = new ArrayList<HandlerRegistration>(); private boolean enabled = false; private State state = State.INACTIVE; private int rowIndex = -1; - private int columnIndex = -1; + private int focusedColumnIndex = -1; private String styleName = null; + /* + * Used to track Grid horizontal scrolling + */ private HandlerRegistration scrollHandler; + /* + * Used to open editor once Grid has vertically scrolled to the proper + * position and data is available + */ + private HandlerRegistration dataAvailableHandler; + private final Button saveButton; private final Button cancelButton; @@ -1209,6 +1345,7 @@ public class Grid<T> extends ResizeComposite implements + " remember to call success() or fail()?"); } }; + private final EditorRequestImpl.RequestCallback<T> bindRequestCallback = new EditorRequestImpl.RequestCallback<T>() { @Override public void onSuccess(EditorRequest<T> request) { @@ -1216,10 +1353,7 @@ public class Grid<T> extends ResizeComposite implements state = State.ACTIVE; bindTimeout.cancel(); - assert rowIndex == request.getRowIndex() : "Request row index " - + request.getRowIndex() - + " did not match the saved row index " + rowIndex; - + rowIndex = request.getRowIndex(); showOverlay(); } } @@ -1227,22 +1361,30 @@ public class Grid<T> extends ResizeComposite implements @Override public void onError(EditorRequest<T> request) { if (state == State.BINDING) { - state = State.INACTIVE; + if (rowIndex == -1) { + doCancel(); + } else { + state = State.ACTIVE; + } bindTimeout.cancel(); // TODO show something in the DOM as well? getLogger().warning( "An error occurred while trying to show the " + "Grid editor"); - grid.getEscalator().setScrollLocked(Direction.VERTICAL, - false); - updateSelectionCheckboxesAsNeeded(true); } } }; /** A set of all the columns that display an error flag. */ private final Set<Column<?, T>> columnErrors = new HashSet<Grid.Column<?, T>>(); + private boolean buffered = true; + + /** Original position of editor */ + private double originalTop; + /** Original scroll position of grid when editor was opened */ + private double originalScrollTop; + private RowHandle<T> pinnedRowHandle; public Editor() { saveButton = new Button(); @@ -1264,7 +1406,9 @@ public class Grid<T> extends ResizeComposite implements }); } - public void setErrorMessage(String errorMessage) { + public void setEditorError(String errorMessage, + Collection<Column<?, T>> errorColumns) { + if (errorMessage == null) { message.removeFromParent(); } else { @@ -1273,6 +1417,17 @@ public class Grid<T> extends ResizeComposite implements messageWrapper.appendChild(message); } } + // In unbuffered mode only show message wrapper if there is an error + if (!isBuffered()) { + setMessageAndButtonsWrapperVisible(errorMessage != null); + } + + if (state == State.ACTIVE || state == State.SAVING) { + for (Column<?, T> c : grid.getColumns()) { + grid.getEditor().setEditorColumnError(c, + errorColumns.contains(c)); + } + } } public int getRow() { @@ -1280,12 +1435,26 @@ public class Grid<T> extends ResizeComposite implements } /** - * Equivalent to {@code editRow(rowIndex, -1)}. + * If a cell of this Grid had focus once this editRow call was + * triggered, the editor component at the previously focused column + * index will be focused. + * + * If a Grid cell was not focused prior to calling this method, it will + * be equivalent to {@code editRow(rowIndex, -1)}. * * @see #editRow(int, int) */ public void editRow(int rowIndex) { - editRow(rowIndex, -1); + // Focus the last focused column in the editor iff grid or its child + // was focused before the edit request + Cell focusedCell = grid.cellFocusHandler.getFocusedCell(); + Element focusedElement = WidgetUtil.getFocusedElement(); + if (focusedCell != null && focusedElement != null + && grid.getElement().isOrHasChild(focusedElement)) { + editRow(rowIndex, focusedCell.getColumn()); + } else { + editRow(rowIndex, -1); + } } /** @@ -1302,28 +1471,41 @@ public class Grid<T> extends ResizeComposite implements * @throws IllegalStateException * if this editor is not enabled * @throws IllegalStateException - * if this editor is already in edit mode + * if this editor is already in edit mode and in buffered + * mode * * @since 7.5 */ - public void editRow(int rowIndex, int columnIndex) { + public void editRow(final int rowIndex, int columnIndex) { if (!enabled) { throw new IllegalStateException( "Cannot edit row: editor is not enabled"); } if (state != State.INACTIVE) { - throw new IllegalStateException( - "Cannot edit row: editor already in edit mode"); + if (isBuffered()) { + throw new IllegalStateException( + "Cannot edit row: editor already in edit mode"); + } } this.rowIndex = rowIndex; - this.columnIndex = columnIndex; - + this.focusedColumnIndex = columnIndex; state = State.ACTIVATING; if (grid.getEscalator().getVisibleRowRange().contains(rowIndex)) { - show(); + show(rowIndex); } else { + hideOverlay(); + dataAvailableHandler = grid + .addDataAvailableHandler(new DataAvailableHandler() { + @Override + public void onDataAvailable(DataAvailableEvent event) { + if (event.getAvailableRows().contains(rowIndex)) { + show(rowIndex); + dataAvailableHandler.removeHandler(); + } + } + }); grid.scrollToRow(rowIndex, ScrollDestination.MIDDLE); } } @@ -1346,14 +1528,18 @@ public class Grid<T> extends ResizeComposite implements throw new IllegalStateException( "Cannot cancel edit: editor is not in edit mode"); } - hideOverlay(); - grid.getEscalator().setScrollLocked(Direction.VERTICAL, false); + handler.cancel(new EditorRequestImpl<T>(grid, rowIndex, null)); + doCancel(); + } - EditorRequest<T> request = new EditorRequestImpl<T>(grid, rowIndex, - null); - handler.cancel(request); + private void doCancel() { + hideOverlay(); state = State.INACTIVE; + rowIndex = -1; + focusedColumnIndex = -1; + grid.getEscalator().setScrollLocked(Direction.VERTICAL, false); updateSelectionCheckboxesAsNeeded(true); + grid.fireEvent(new EditorCloseEvent(grid.eventCell)); } private void updateSelectionCheckboxesAsNeeded(boolean isEnabled) { @@ -1446,14 +1632,15 @@ public class Grid<T> extends ResizeComposite implements this.enabled = enabled; } - protected void show() { + protected void show(int rowIndex) { if (state == State.ACTIVATING) { state = State.BINDING; bindTimeout.schedule(BIND_TIMEOUT_MS); EditorRequest<T> request = new EditorRequestImpl<T>(grid, rowIndex, bindRequestCallback); handler.bind(request); - grid.getEscalator().setScrollLocked(Direction.VERTICAL, true); + grid.getEscalator().setScrollLocked(Direction.VERTICAL, + isBuffered()); updateSelectionCheckboxesAsNeeded(false); } } @@ -1463,15 +1650,6 @@ public class Grid<T> extends ResizeComposite implements assert this.grid == null : "Can only attach editor to Grid once"; this.grid = grid; - - grid.addDataAvailableHandler(new DataAvailableHandler() { - @Override - public void onDataAvailable(DataAvailableEvent event) { - if (event.getAvailableRows().contains(rowIndex)) { - show(); - } - } - }); } protected State getState() { @@ -1516,6 +1694,8 @@ public class Grid<T> extends ResizeComposite implements * @since 7.5 */ protected void showOverlay() { + // Ensure overlay is hidden initially + hideOverlay(); DivElement gridElement = DivElement.as(grid.getElement()); @@ -1526,19 +1706,38 @@ public class Grid<T> extends ResizeComposite implements @Override public void onScroll(ScrollEvent event) { updateHorizontalScrollPosition(); + if (!isBuffered()) { + updateVerticalScrollPosition(); + } } }); gridElement.appendChild(editorOverlay); + editorOverlay.appendChild(frozenCellWrapper); editorOverlay.appendChild(cellWrapper); editorOverlay.appendChild(messageAndButtonsWrapper); + int frozenColumns = grid.getVisibleFrozenColumnCount(); + double frozenColumnsWidth = 0; + double cellHeight = 0; + for (int i = 0; i < tr.getCells().getLength(); i++) { Element cell = createCell(tr.getCells().getItem(i)); - - cellWrapper.appendChild(cell); + cellHeight = Math.max(cellHeight, WidgetUtil + .getRequiredHeightBoundingClientRectDouble(tr + .getCells().getItem(i))); Column<?, T> column = grid.getVisibleColumn(i); + + if (i < frozenColumns) { + frozenCellWrapper.appendChild(cell); + frozenColumnsWidth += WidgetUtil + .getRequiredWidthBoundingClientRectDouble(tr + .getCells().getItem(i)); + } else { + cellWrapper.appendChild(cell); + } + if (column.isEditable()) { Widget editor = getHandler().getWidget(column); @@ -1547,7 +1746,28 @@ public class Grid<T> extends ResizeComposite implements attachWidget(editor, cell); } - if (i == columnIndex) { + final int currentColumnIndex = i; + if (editor instanceof HasFocusHandlers) { + // Use a proper focus handler if available + focusHandlers.add(((HasFocusHandlers) editor) + .addFocusHandler(new FocusHandler() { + @Override + public void onFocus(FocusEvent event) { + focusedColumnIndex = currentColumnIndex; + } + })); + } else { + // Try sniffing for DOM focus events + focusHandlers.add(editor.addDomHandler( + new FocusHandler() { + @Override + public void onFocus(FocusEvent event) { + focusedColumnIndex = currentColumnIndex; + } + }, FocusEvent.getType())); + } + + if (i == focusedColumnIndex) { if (editor instanceof Focusable) { ((Focusable) editor).focus(); } else if (editor instanceof com.google.gwt.user.client.ui.Focusable) { @@ -1557,17 +1777,70 @@ public class Grid<T> extends ResizeComposite implements } } else { cell.addClassName(NOT_EDITABLE_CLASS_NAME); + cell.addClassName(tr.getCells().getItem(i).getClassName()); + // If the focused or frozen stylename is present it should + // not be inherited by the editor cell as it is not useful + // in the editor and would look broken without additional + // style rules. This is a bit of a hack. + cell.removeClassName(grid.cellFocusStyleName); + cell.removeClassName("frozen"); + + if (column == grid.selectionColumn) { + // Duplicate selection column CheckBox + + pinnedRowHandle = grid.getDataSource().getHandle( + grid.getDataSource().getRow(rowIndex)); + pinnedRowHandle.pin(); + + // We need to duplicate the selection CheckBox for the + // editor overlay since the original one is hidden by + // the overlay + final CheckBox checkBox = GWT.create(CheckBox.class); + checkBox.setValue(grid.isSelected(pinnedRowHandle + .getRow())); + checkBox.sinkEvents(Event.ONCLICK); + + checkBox.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + T row = pinnedRowHandle.getRow(); + if (grid.isSelected(row)) { + grid.deselect(row); + } else { + grid.select(row); + } + } + }); + attachWidget(checkBox, cell); + columnToWidget.put(column, checkBox); + + // Only enable CheckBox in non-buffered mode + checkBox.setEnabled(!isBuffered()); + + } else if (!(column.getRenderer() instanceof WidgetRenderer)) { + // Copy non-widget content directly + cell.setInnerHTML(tr.getCells().getItem(i) + .getInnerHTML()); + } } } + setBounds(frozenCellWrapper, 0, 0, frozenColumnsWidth, 0); + setBounds(cellWrapper, frozenColumnsWidth, 0, tr.getOffsetWidth() + - frozenColumnsWidth, cellHeight); + // Only add these elements once if (!messageAndButtonsWrapper.isOrHasChild(messageWrapper)) { messageAndButtonsWrapper.appendChild(messageWrapper); messageAndButtonsWrapper.appendChild(buttonsWrapper); } - attachWidget(saveButton, buttonsWrapper); - attachWidget(cancelButton, buttonsWrapper); + if (isBuffered()) { + attachWidget(saveButton, buttonsWrapper); + attachWidget(cancelButton, buttonsWrapper); + } + + setMessageAndButtonsWrapperVisible(isBuffered()); updateHorizontalScrollPosition(); @@ -1579,9 +1852,11 @@ public class Grid<T> extends ResizeComposite implements int gridTop = gridElement.getAbsoluteTop(); double overlayTop = rowTop + bodyTop - gridTop; - if (buttonsShouldBeRenderedBelow(tr)) { + originalScrollTop = grid.getScrollTop(); + if (!isBuffered() || buttonsShouldBeRenderedBelow(tr)) { // Default case, editor buttons are below the edited row editorOverlay.getStyle().setTop(overlayTop, Unit.PX); + originalTop = overlayTop; editorOverlay.getStyle().clearBottom(); } else { // Move message and buttons wrapper on top of cell wrapper if @@ -1614,6 +1889,20 @@ public class Grid<T> extends ResizeComposite implements } protected void hideOverlay() { + if (editorOverlay.getParentElement() == null) { + return; + } + + if (pinnedRowHandle != null) { + pinnedRowHandle.unpin(); + pinnedRowHandle = null; + } + + for (HandlerRegistration r : focusHandlers) { + r.removeHandler(); + } + focusHandlers.clear(); + for (Widget w : columnToWidget.values()) { setParent(w, null); } @@ -1624,11 +1913,16 @@ public class Grid<T> extends ResizeComposite implements editorOverlay.removeAllChildren(); cellWrapper.removeAllChildren(); + frozenCellWrapper.removeAllChildren(); editorOverlay.removeFromParent(); scrollHandler.removeHandler(); clearEditorColumnErrors(); + + if (focusedColumnIndex != -1) { + grid.focusCell(rowIndex, focusedColumnIndex); + } } protected void setStylePrimaryName(String primaryName) { @@ -1636,6 +1930,7 @@ public class Grid<T> extends ResizeComposite implements editorOverlay.removeClassName(styleName); cellWrapper.removeClassName(styleName + "-cells"); + frozenCellWrapper.removeClassName(styleName + "-cells"); messageAndButtonsWrapper.removeClassName(styleName + "-footer"); messageWrapper.removeClassName(styleName + "-message"); @@ -1648,6 +1943,7 @@ public class Grid<T> extends ResizeComposite implements editorOverlay.setClassName(styleName); cellWrapper.setClassName(styleName + "-cells"); + frozenCellWrapper.setClassName(styleName + "-cells frozen"); messageAndButtonsWrapper.setClassName(styleName + "-footer"); messageWrapper.setClassName(styleName + "-message"); @@ -1698,7 +1994,37 @@ public class Grid<T> extends ResizeComposite implements private void updateHorizontalScrollPosition() { double scrollLeft = grid.getScrollLeft(); - cellWrapper.getStyle().setLeft(-scrollLeft, Unit.PX); + cellWrapper.getStyle().setLeft( + frozenCellWrapper.getOffsetWidth() - scrollLeft, Unit.PX); + } + + /** + * Moves the editor overlay on scroll so that it stays on top of the + * edited row. This will also snap the editor to top or bottom of the + * row container if the edited row is scrolled out of the visible area. + */ + private void updateVerticalScrollPosition() { + double newScrollTop = grid.getScrollTop(); + + int gridTop = grid.getElement().getAbsoluteTop(); + int editorHeight = editorOverlay.getOffsetHeight(); + + Escalator escalator = grid.getEscalator(); + TableSectionElement header = escalator.getHeader().getElement(); + int footerTop = escalator.getFooter().getElement().getAbsoluteTop(); + int headerBottom = header.getAbsoluteBottom(); + + double newTop = originalTop - (newScrollTop - originalScrollTop); + + if (newTop + gridTop < headerBottom) { + // Snap editor to top of the row container + newTop = header.getOffsetHeight(); + } else if (newTop + gridTop > footerTop - editorHeight) { + // Snap editor to the bottom of the row container + newTop = footerTop - editorHeight - gridTop; + } + + editorOverlay.getStyle().setTop(newTop, Unit.PX); } protected void setGridEnabled(boolean enabled) { @@ -1777,6 +2103,44 @@ public class Grid<T> extends ResizeComposite implements public boolean isEditorColumnError(Column<?, T> column) { return columnErrors.contains(column); } + + public void setBuffered(boolean buffered) { + this.buffered = buffered; + setMessageAndButtonsWrapperVisible(buffered); + } + + public boolean isBuffered() { + return buffered; + } + + private void setMessageAndButtonsWrapperVisible(boolean visible) { + if (visible) { + messageAndButtonsWrapper.getStyle().clearDisplay(); + } else { + messageAndButtonsWrapper.getStyle().setDisplay(Display.NONE); + } + } + + /** + * Sets the event handler for this Editor. + * + * @since + * @param handler + * the new event handler + */ + public void setEventHandler(EventHandler<T> handler) { + eventHandler = handler; + } + + /** + * Returns the event handler of this Editor. + * + * @since + * @return the current event handler + */ + public EventHandler<T> getEventHandler() { + return eventHandler; + } } public static abstract class AbstractGridKeyEvent<HANDLER extends AbstractGridKeyEventHandler> @@ -2351,6 +2715,7 @@ public class Grid<T> extends ResizeComposite implements private boolean initDone = false; private boolean selected = false; + private CheckBox selectAllCheckBox; SelectionColumn(final Renderer<Boolean> selectColumnRenderer) { super(selectColumnRenderer); @@ -2375,41 +2740,57 @@ public class Grid<T> extends ResizeComposite implements * exist. */ final SelectionModel.Multi<T> model = (Multi<T>) getSelectionModel(); - final CheckBox checkBox = GWT.create(CheckBox.class); - checkBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() { - @Override - public void onValueChange(ValueChangeEvent<Boolean> event) { - if (event.getValue()) { - fireEvent(new SelectAllEvent<T>(model)); - selected = true; - } else { - model.deselectAll(); - selected = false; - } - } - }); - checkBox.setValue(selected); - selectionCell.setWidget(checkBox); - // Select all with space when "select all" cell is active - addHeaderKeyUpHandler(new HeaderKeyUpHandler() { - @Override - public void onKeyUp(GridKeyUpEvent event) { - if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE) { - return; - } - HeaderRow targetHeaderRow = getHeader().getRow( - event.getFocusedCell().getRowIndex()); - if (!targetHeaderRow.isDefault()) { - return; + if (selectAllCheckBox == null) { + selectAllCheckBox = GWT.create(CheckBox.class); + selectAllCheckBox.setStylePrimaryName(getStylePrimaryName() + + SELECT_ALL_CHECKBOX_CLASSNAME); + selectAllCheckBox + .addValueChangeHandler(new ValueChangeHandler<Boolean>() { + + @Override + public void onValueChange( + ValueChangeEvent<Boolean> event) { + if (event.getValue()) { + fireEvent(new SelectAllEvent<T>(model)); + selected = true; + } else { + model.deselectAll(); + selected = false; + } + } + }); + selectAllCheckBox.setValue(selected); + + // Select all with space when "select all" cell is active + addHeaderKeyUpHandler(new HeaderKeyUpHandler() { + @Override + public void onKeyUp(GridKeyUpEvent event) { + if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE) { + return; + } + HeaderRow targetHeaderRow = getHeader().getRow( + event.getFocusedCell().getRowIndex()); + if (!targetHeaderRow.isDefault()) { + return; + } + if (event.getFocusedCell().getColumn() == SelectionColumn.this) { + // Send events to ensure state is updated + selectAllCheckBox.setValue( + !selectAllCheckBox.getValue(), true); + } } - if (event.getFocusedCell().getColumn() == SelectionColumn.this) { - // Send events to ensure row selection state is - // updated - checkBox.setValue(!checkBox.getValue(), true); + }); + } else { + for (HeaderRow row : header.getRows()) { + if (row.getCell(this).getType() == GridStaticCellType.WIDGET) { + // Detach from old header. + row.getCell(this).setText(""); } } - }); + } + + selectionCell.setWidget(selectAllCheckBox); } @Override @@ -2471,7 +2852,6 @@ public class Grid<T> extends ResizeComposite implements super.setEditable(editable); return this; } - } /** @@ -2736,7 +3116,9 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; - final double widthFixed = Math.max(widthAsIs, + // Check for max width just to be sure we don't break the limits + final double widthFixed = Math.max( + Math.min(getMaxWidth(column), widthAsIs), column.getMinimumWidth()); defaultExpandRatios = defaultExpandRatios && (column.getExpandRatio() == -1 || column == selectionColumn); @@ -2755,8 +3137,9 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : nonFixedColumns) { final int expandRatio = (defaultExpandRatios ? 1 : column .getExpandRatio()); - final double newWidth = column.getWidthActual(); final double maxWidth = getMaxWidth(column); + final double newWidth = Math.min(maxWidth, + column.getWidthActual()); boolean shouldExpand = newWidth < maxWidth && expandRatio > 0 && column != selectionColumn; if (shouldExpand) { @@ -2775,6 +3158,10 @@ public class Grid<T> extends ResizeComposite implements double pixelsToDistribute = escalator.getInnerWidth() - reservedPixels; if (pixelsToDistribute <= 0 || totalRatios <= 0) { + if (pixelsToDistribute <= 0) { + // Set column sizes for expanding columns + setColumnSizes(columnSizes); + } return; } @@ -3222,7 +3609,6 @@ public class Grid<T> extends ResizeComposite implements clickOutsideToCloseHandlerRegistration = Event .addNativePreviewHandler(clickOutsideToCloseHandler); } - openCloseButton.setHeight(""); } /** @@ -3251,46 +3637,6 @@ public class Grid<T> extends ResizeComposite implements return content.getParent() == rootContainer; } - /** - * Adds or moves the given widget to the end of the sidebar. - * - * @param widget - * the widget to add or move - */ - public void add(Widget widget) { - content.add(widget); - updateVisibility(); - } - - /** - * Removes the given widget from the sidebar. - * - * @param widget - * the widget to remove - */ - public void remove(Widget widget) { - content.remove(widget); - // updateVisibility is called by remove listener - } - - /** - * Inserts given widget to the given index inside the sidebar. If the - * widget is already in the sidebar, then it is moved to the new index. - * <p> - * See - * {@link FlowPanel#insert(com.google.gwt.user.client.ui.IsWidget, int)} - * for further details. - * - * @param widget - * the widget to insert - * @param beforeIndex - * 0-based index position for the widget. - */ - public void insert(Widget widget, int beforeIndex) { - content.insert(widget, beforeIndex); - updateVisibility(); - } - @Override public void setStylePrimaryName(String styleName) { super.setStylePrimaryName(styleName); @@ -3536,10 +3882,6 @@ public class Grid<T> extends ResizeComposite implements private final AutoColumnWidthsRecalculator autoColumnWidthsRecalculator = new AutoColumnWidthsRecalculator(); private boolean enabled = true; - private double lastTouchEventTime = 0; - private int lastTouchEventX = -1; - private int lastTouchEventY = -1; - private int lastTouchEventRow = -1; private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater(); @@ -3725,8 +4067,8 @@ public class Grid<T> extends ResizeComposite implements } private boolean isSidebarOnDraggedRow() { - return eventCell.getRowIndex() == 0 && getSidebar().isInDOM() - && !getSidebar().isOpen(); + return eventCell.getRowIndex() == 0 && sidebar.isInDOM() + && !sidebar.isOpen(); } /** @@ -3736,8 +4078,7 @@ public class Grid<T> extends ResizeComposite implements private double getSidebarBoundaryComparedTo(double left) { if (isSidebarOnDraggedRow()) { double absoluteLeft = left + getElement().getAbsoluteLeft(); - double sidebarLeft = getSidebar().getElement() - .getAbsoluteLeft(); + double sidebarLeft = sidebar.getElement().getAbsoluteLeft(); double diff = absoluteLeft - sidebarLeft; if (diff > 0) { @@ -4236,6 +4577,16 @@ public class Grid<T> extends ResizeComposite implements return this; } + /** + * Returns the current header caption for this column + * + * @since + * @return the header caption string + */ + public String getHeaderCaption() { + return headerCaption; + } + private void updateHeader() { HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { @@ -4937,7 +5288,7 @@ public class Grid<T> extends ResizeComposite implements @Override public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) { for (FlyweightCell cell : cellsToDetach) { - Renderer renderer = findRenderer(cell); + Renderer<?> renderer = findRenderer(cell); if (renderer instanceof WidgetRenderer) { try { Widget w = WidgetUtil.findWidget(cell.getElement() @@ -4967,7 +5318,7 @@ public class Grid<T> extends ResizeComposite implements // any more rowReference.set(rowIndex, null, row.getElement()); for (FlyweightCell cell : detachedCells) { - Renderer renderer = findRenderer(cell); + Renderer<?> renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { Column<?, T> column = getVisibleColumn(cell.getColumn()); @@ -5211,7 +5562,7 @@ public class Grid<T> extends ResizeComposite implements sinkEvents(getHeader().getConsumedEvents()); sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP, BrowserEvents.KEYPRESS, BrowserEvents.DBLCLICK, - BrowserEvents.MOUSEDOWN)); + BrowserEvents.MOUSEDOWN, BrowserEvents.CLICK)); // Make ENTER and SHIFT+ENTER in the header perform sorting addHeaderKeyUpHandler(new HeaderKeyUpHandler() { @@ -5353,6 +5704,35 @@ public class Grid<T> extends ResizeComposite implements } /** + * Try to focus a cell by row and column index. Purely internal method. + * + * @param rowIndex + * row index from Editor + * @param columnIndex + * column index from Editor + */ + void focusCell(int rowIndex, int columnIndex) { + RowReference<T> row = new RowReference<T>(this); + Range visibleRows = escalator.getVisibleRowRange(); + + TableRowElement rowElement; + if (visibleRows.contains(rowIndex)) { + rowElement = escalator.getBody().getRowElement(rowIndex); + } else { + getLogger().warning("Row index was not among visible row range"); + return; + } + row.set(rowIndex, dataSource.getRow(rowIndex), rowElement); + + CellReference<T> cell = new CellReference<T>(row); + cell.set(columnIndex, columnIndex, getVisibleColumn(columnIndex)); + + cellFocusHandler.setCellFocus(cell); + + WidgetUtil.focus(getElement()); + } + + /** * Refreshes all header rows */ void refreshHeader() { @@ -5883,10 +6263,23 @@ public class Grid<T> extends ResizeComposite implements return footer.isVisible(); } - protected Editor<T> getEditor() { + public Editor<T> getEditor() { return editor; } + /** + * Add handler for editor open/move/close events + * + * @param handler + * editor handler object + * @return a {@link HandlerRegistration} object that can be used to remove + * the event handler + */ + public HandlerRegistration addEditorEventHandler(EditorEventHandler handler) { + return addHandler(handler, EditorEvent.TYPE); + + } + protected Escalator getEscalator() { return escalator; } @@ -6051,7 +6444,12 @@ public class Grid<T> extends ResizeComposite implements } private void updateFrozenColumns() { - int numberOfColumns = frozenColumnCount; + escalator.getColumnConfiguration().setFrozenColumnCount( + getVisibleFrozenColumnCount()); + } + + private int getVisibleFrozenColumnCount() { + int numberOfColumns = getFrozenColumnCount(); // for the escalator the hidden columns are not in the frozen column // count, but for grid they are. thus need to convert the index @@ -6066,9 +6464,7 @@ public class Grid<T> extends ResizeComposite implements } else if (selectionColumn != null) { numberOfColumns++; } - - escalator.getColumnConfiguration() - .setFrozenColumnCount(numberOfColumns); + return numberOfColumns; } /** @@ -6347,6 +6743,14 @@ public class Grid<T> extends ResizeComposite implements return; } + String eventType = event.getType(); + + if (eventType.equals(BrowserEvents.FOCUS) + || eventType.equals(BrowserEvents.BLUR)) { + super.onBrowserEvent(event); + return; + } + EventTarget target = event.getEventTarget(); if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) { @@ -6357,7 +6761,6 @@ public class Grid<T> extends ResizeComposite implements RowContainer container = escalator.findRowContainer(e); Cell cell; - String eventType = event.getType(); if (container == null) { if (eventType.equals(BrowserEvents.KEYDOWN) || eventType.equals(BrowserEvents.KEYUP) @@ -6387,7 +6790,7 @@ public class Grid<T> extends ResizeComposite implements eventCell.set(cell, getSectionFromContainer(container)); // Editor can steal focus from Grid and is still handled - if (handleEditorEvent(event, container)) { + if (isEditorEnabled() && handleEditorEvent(event, container)) { return; } @@ -6465,50 +6868,8 @@ public class Grid<T> extends ResizeComposite implements } private boolean handleEditorEvent(Event event, RowContainer container) { - - final boolean closeEvent = event.getTypeInt() == Event.ONKEYDOWN - && event.getKeyCode() == Editor.KEYCODE_HIDE; - - double now = Duration.currentTimeMillis(); - int currentX = WidgetUtil.getTouchOrMouseClientX(event); - int currentY = WidgetUtil.getTouchOrMouseClientY(event); - - final boolean validTouchOpenEvent = event.getTypeInt() == Event.ONTOUCHEND - && now - lastTouchEventTime < 500 - && lastTouchEventRow == eventCell.getRowIndex() - && Math.abs(lastTouchEventX - currentX) < 20 - && Math.abs(lastTouchEventY - currentY) < 20; - - final boolean openEvent = event.getTypeInt() == Event.ONDBLCLICK - || (event.getTypeInt() == Event.ONKEYDOWN && event.getKeyCode() == Editor.KEYCODE_SHOW) - || validTouchOpenEvent; - - if (event.getTypeInt() == Event.ONTOUCHSTART) { - lastTouchEventX = currentX; - lastTouchEventY = currentY; - } - - if (event.getTypeInt() == Event.ONTOUCHEND) { - lastTouchEventTime = now; - lastTouchEventRow = eventCell.getRowIndex(); - } - - if (editor.getState() != Editor.State.INACTIVE) { - if (closeEvent) { - editor.cancel(); - FocusUtil.setFocus(this, true); - } - return true; - } - - if (container == escalator.getBody() && editor.isEnabled() && openEvent) { - editor.editRow(eventCell.getRowIndex(), - eventCell.getColumnIndexDOM()); - event.preventDefault(); - return true; - } - - return false; + return getEditor().getEventHandler().handleEvent( + new EditorDomEvent<T>(event, getEventCell())); } private boolean handleRendererEvent(Event event, RowContainer container) { @@ -6703,11 +7064,21 @@ public class Grid<T> extends ResizeComposite implements if (args.getIndicesLength() == 0) { return editor.editorOverlay; - } else if (args.getIndicesLength() == 1 - && args.getIndex(0) < columns.size()) { - escalator - .scrollToColumn(args.getIndex(0), ScrollDestination.ANY, 0); - return editor.getWidget(columns.get(args.getIndex(0))).getElement(); + } else if (args.getIndicesLength() == 1) { + int index = args.getIndex(0); + if (index >= columns.size()) { + return null; + } + + escalator.scrollToColumn(index, ScrollDestination.ANY, 0); + Widget widget = editor.getWidget(columns.get(index)); + + if (widget != null) { + return widget.getElement(); + } + + // No widget for the column. + return null; } return null; @@ -6863,12 +7234,12 @@ public class Grid<T> extends ResizeComposite implements * 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); + return ((SelectionModel.Multi<T>) selectionModel) + .select(Collections.singleton(row)); } else { throw new IllegalStateException("Unsupported selection model"); } @@ -6888,12 +7259,12 @@ public class Grid<T> extends ResizeComposite implements * 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); + return ((SelectionModel.Multi<T>) selectionModel) + .deselect(Collections.singleton(row)); } else { throw new IllegalStateException("Unsupported selection model"); } @@ -7670,6 +8041,12 @@ public class Grid<T> extends ResizeComposite implements if (escalator.getInnerWidth() != autoColumnWidthsRecalculator.lastCalculatedInnerWidth) { recalculateColumnWidths(); } + + // Vertical resizing could make editor positioning invalid so it + // needs to be recalculated on resize + if (isEditorActive()) { + editor.updateVerticalScrollPosition(); + } } }); } @@ -7775,15 +8152,15 @@ public class Grid<T> extends ResizeComposite implements @Override protected void doAttachChildren() { - if (getSidebar().getParent() == this) { - onAttach(getSidebar()); + if (sidebar.getParent() == this) { + onAttach(sidebar); } } @Override protected void doDetachChildren() { - if (getSidebar().getParent() == this) { - onDetach(getSidebar()); + if (sidebar.getParent() == this) { + onDetach(sidebar); } } @@ -7815,6 +8192,10 @@ public class Grid<T> extends ResizeComposite implements "Details generator may not be null"); } + for (Integer index : visibleDetails) { + setDetailsVisible(index, false); + } + this.detailsGenerator = detailsGenerator; // this will refresh all visible spacers @@ -7846,6 +8227,10 @@ public class Grid<T> extends ResizeComposite implements * @see #isDetailsVisible(int) */ public void setDetailsVisible(int rowIndex, boolean visible) { + if (DetailsGenerator.NULL.equals(detailsGenerator)) { + return; + } + Integer rowIndexInteger = Integer.valueOf(rowIndex); /* @@ -7904,19 +8289,6 @@ public class Grid<T> extends ResizeComposite implements } /** - * Returns the sidebar for this grid. - * <p> - * The grid's sidebar shows the column hiding options for those columns that - * have been set as {@link Column#setHidable(boolean) hidable}. - * - * @since 7.5.0 - * @return the sidebar widget for this grid - */ - private Sidebar getSidebar() { - return sidebar; - } - - /** * Gets the customizable menu bar that is by default used for toggling * column hidability. The application developer is allowed to add their * custom items to the end of the menu, but should try to avoid modifying @@ -7962,6 +8334,54 @@ public class Grid<T> extends ResizeComposite implements } } + @Override + public int getTabIndex() { + return FocusUtil.getTabIndex(this); + } + + @Override + public void setAccessKey(char key) { + FocusUtil.setAccessKey(this, key); + } + + @Override + public void setFocus(boolean focused) { + FocusUtil.setFocus(this, focused); + } + + @Override + public void setTabIndex(int index) { + FocusUtil.setTabIndex(this, index); + } + + @Override + public void focus() { + setFocus(true); + } + + /** + * Sets the buffered editor mode. + * + * @since 7.6 + * @param editorUnbuffered + * <code>true</code> to enable buffered editor, + * <code>false</code> to disable it + */ + public void setEditorBuffered(boolean editorBuffered) { + editor.setBuffered(editorBuffered); + } + + /** + * Gets the buffered editor mode. + * + * @since 7.6 + * @return <code>true</code> if buffered editor is enabled, + * <code>false</code> otherwise + */ + public boolean isEditorBuffered() { + return editor.isBuffered(); + } + /** * Returns the {@link EventCellReference} for the latest event fired from * this Grid. @@ -7974,4 +8394,26 @@ public class Grid<T> extends ResizeComposite implements public EventCellReference<T> getEventCell() { return eventCell; } + + /** + * Returns a CellReference for the cell to which the given element belongs + * to. + * + * @since 7.6 + * @param element + * Element to find from the cell's content. + * @return CellReference or <code>null</code> if cell was not found. + */ + public CellReference<T> getCellReference(Element element) { + RowContainer container = getEscalator().findRowContainer(element); + if (container != null) { + Cell cell = container.getCell(element); + if (cell != null) { + EventCellReference<T> cellRef = new EventCellReference<T>(this); + cellRef.set(cell, getSectionFromContainer(container)); + return cellRef; + } + } + return null; + } } diff --git a/client/tests/src/com/vaadin/client/VBrowserDetailsUserAgentParserTest.java b/client/tests/src/com/vaadin/client/VBrowserDetailsUserAgentParserTest.java index 62b727e5f5..24bf9b6558 100644 --- a/client/tests/src/com/vaadin/client/VBrowserDetailsUserAgentParserTest.java +++ b/client/tests/src/com/vaadin/client/VBrowserDetailsUserAgentParserTest.java @@ -56,6 +56,8 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { private static final String ANDROID_MOTOROLA_3_0 = "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13"; private static final String ANDROID_GALAXY_NEXUS_4_0_4_CHROME = "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"; + private static final String EDGE_WINDOWS_10 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"; + public void testSafari3() { VBrowserDetails bd = new VBrowserDetails(SAFARI3_WINDOWS); assertWebKit(bd); @@ -423,6 +425,14 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { assertWindows(bd, true); } + public void testEdgeWindows10() { + VBrowserDetails bd = new VBrowserDetails(EDGE_WINDOWS_10); + assertEdge(bd); + assertBrowserMajorVersion(bd, 12); + assertBrowserMinorVersion(bd, 10240); + assertWindows(bd, false); + } + /* * Helper methods below */ @@ -484,6 +494,7 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { assertFalse(browserDetails.isIE()); assertFalse(browserDetails.isOpera()); assertFalse(browserDetails.isSafari()); + assertFalse(browserDetails.isEdge()); } private void assertChrome(VBrowserDetails browserDetails) { @@ -493,6 +504,7 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { assertFalse(browserDetails.isIE()); assertFalse(browserDetails.isOpera()); assertFalse(browserDetails.isSafari()); + assertFalse(browserDetails.isEdge()); } private void assertIE(VBrowserDetails browserDetails) { @@ -502,6 +514,7 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { assertTrue(browserDetails.isIE()); assertFalse(browserDetails.isOpera()); assertFalse(browserDetails.isSafari()); + assertFalse(browserDetails.isEdge()); } private void assertOpera(VBrowserDetails browserDetails) { @@ -511,6 +524,7 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { assertFalse(browserDetails.isIE()); assertTrue(browserDetails.isOpera()); assertFalse(browserDetails.isSafari()); + assertFalse(browserDetails.isEdge()); } private void assertSafari(VBrowserDetails browserDetails) { @@ -520,6 +534,17 @@ public class VBrowserDetailsUserAgentParserTest extends TestCase { assertFalse(browserDetails.isIE()); assertFalse(browserDetails.isOpera()); assertTrue(browserDetails.isSafari()); + assertFalse(browserDetails.isEdge()); + } + + private void assertEdge(VBrowserDetails browserDetails) { + // Browser + assertFalse(browserDetails.isFirefox()); + assertFalse(browserDetails.isChrome()); + assertFalse(browserDetails.isIE()); + assertFalse(browserDetails.isOpera()); + assertFalse(browserDetails.isSafari()); + assertTrue(browserDetails.isEdge()); } private void assertMacOSX(VBrowserDetails browserDetails) { |