diff options
author | Pekka Hyvönen <pekka@vaadin.com> | 2015-04-30 13:40:45 +0300 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2015-04-30 13:40:52 +0300 |
commit | ef9fb276a93d1a7967ef0e6405dee56c8aa2a148 (patch) | |
tree | 622280f6e6e2904fef61c3483fe36e14ab6b924d /client | |
parent | a9bc160aee00b93340ac3f047505e032d4a696b0 (diff) | |
parent | bbf30fff168fd4a9552d23c8341e27aa1821884b (diff) | |
download | vaadin-framework-ef9fb276a93d1a7967ef0e6405dee56c8aa2a148.tar.gz vaadin-framework-ef9fb276a93d1a7967ef0e6405dee56c8aa2a148.zip |
Merge branch 'grid-7.5'
Change-Id: Ifa976fa4be1258fd35999de17775da70afedb2a8
Diffstat (limited to 'client')
21 files changed, 5416 insertions, 557 deletions
diff --git a/client/src/com/vaadin/client/WidgetUtil.java b/client/src/com/vaadin/client/WidgetUtil.java index 5f88f6da46..a9cd23c841 100644 --- a/client/src/com/vaadin/client/WidgetUtil.java +++ b/client/src/com/vaadin/client/WidgetUtil.java @@ -1459,4 +1459,124 @@ public class WidgetUtil { return Logger.getLogger(WidgetUtil.class.getName()); } + /** + * Returns the thickness of the given element's top border. + * <p> + * The value is determined using computed style when available and + * calculated otherwise. + * + * @since + * @param element + * the element to measure + * @return the top border thickness + */ + public static double getBorderTopThickness(Element element) { + return getBorderThickness(element, new String[] { "borderTopWidth" }); + } + + /** + * Returns the thickness of the given element's bottom border. + * <p> + * The value is determined using computed style when available and + * calculated otherwise. + * + * @since + * @param element + * the element to measure + * @return the bottom border thickness + */ + public static double getBorderBottomThickness(Element element) { + return getBorderThickness(element, new String[] { "borderBottomWidth" }); + } + + /** + * Returns the combined thickness of the given element's top and bottom + * borders. + * <p> + * The value is determined using computed style when available and + * calculated otherwise. + * + * @since + * @param element + * the element to measure + * @return the top and bottom border thickness + */ + public static double getBorderTopAndBottomThickness(Element element) { + return getBorderThickness(element, new String[] { "borderTopWidth", + "borderBottomWidth" }); + } + + /** + * Returns the thickness of the given element's left border. + * <p> + * The value is determined using computed style when available and + * calculated otherwise. + * + * @since + * @param element + * the element to measure + * @return the left border thickness + */ + public static double getBorderLeftThickness(Element element) { + return getBorderThickness(element, new String[] { "borderLeftWidth" }); + } + + /** + * Returns the thickness of the given element's right border. + * <p> + * The value is determined using computed style when available and + * calculated otherwise. + * + * @since + * @param element + * the element to measure + * @return the right border thickness + */ + public static double getBorderRightThickness(Element element) { + return getBorderThickness(element, new String[] { "borderRightWidth" }); + } + + /** + * Returns the thickness of the given element's left and right borders. + * <p> + * The value is determined using computed style when available and + * calculated otherwise. + * + * @since + * @param element + * the element to measure + * @return the top border thickness + */ + public static double getBorderLeftAndRightThickness(Element element) { + return getBorderThickness(element, new String[] { "borderLeftWidth", + "borderRightWidth" }); + } + + private static native double getBorderThickness( + com.google.gwt.dom.client.Element element, String[] borderNames) + /*-{ + if (typeof $wnd.getComputedStyle === 'function') { + var computedStyle = $wnd.getComputedStyle(element); + var width = 0; + for (i=0; i< borderNames.length; i++) { + var borderWidth = computedStyle[borderNames[i]]; + width += parseFloat(borderWidth); + } + return width; + } else { + var parentElement = element.offsetParent; + var cloneElement = element.cloneNode(false); + cloneElement.style.boxSizing ="content-box"; + parentElement.appendChild(cloneElement); + cloneElement.style.height = "10px"; // IE8 wants the height to be set to something... + var heightWithBorder = cloneElement.offsetHeight; + for (i=0; i< borderNames.length; i++) { + cloneElement.style[borderNames[i]] = "0"; + } + var heightWithoutBorder = cloneElement.offsetHeight; + parentElement.removeChild(cloneElement); + + return heightWithBorder - heightWithoutBorder; + } + }-*/; } diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index 5e9dfc6101..d31baaa665 100644 --- a/client/src/com/vaadin/client/connectors/GridConnector.java +++ b/client/src/com/vaadin/client/connectors/GridConnector.java @@ -19,6 +19,7 @@ package com.vaadin.client.connectors; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -31,12 +32,15 @@ import java.util.logging.Logger; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.DeferredWorker; import com.vaadin.client.MouseEventDetailsBuilder; import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.connectors.RpcDataSourceConnector.DetailsListener; import com.vaadin.client.connectors.RpcDataSourceConnector.RpcDataSource; import com.vaadin.client.data.DataSource.RowHandle; import com.vaadin.client.renderers.Renderer; @@ -45,11 +49,16 @@ import com.vaadin.client.ui.AbstractHasComponentsConnector; import com.vaadin.client.ui.SimpleManagedLayout; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; +import com.vaadin.client.widget.grid.DetailsGenerator; import com.vaadin.client.widget.grid.EditorHandler; import com.vaadin.client.widget.grid.RowReference; import com.vaadin.client.widget.grid.RowStyleGenerator; import com.vaadin.client.widget.grid.events.BodyClickHandler; import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler; +import com.vaadin.client.widget.grid.events.ColumnReorderEvent; +import com.vaadin.client.widget.grid.events.ColumnReorderHandler; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler; import com.vaadin.client.widget.grid.events.GridClickEvent; import com.vaadin.client.widget.grid.events.GridDoubleClickEvent; import com.vaadin.client.widget.grid.events.SelectAllEvent; @@ -70,8 +79,10 @@ import com.vaadin.client.widgets.Grid.FooterCell; import com.vaadin.client.widgets.Grid.FooterRow; import com.vaadin.client.widgets.Grid.HeaderCell; import com.vaadin.client.widgets.Grid.HeaderRow; +import com.vaadin.shared.Connector; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.DetailsConnectorChange; import com.vaadin.shared.ui.grid.EditorClientRpc; import com.vaadin.shared.ui.grid.EditorServerRpc; import com.vaadin.shared.ui.grid.GridClientRpc; @@ -101,7 +112,7 @@ import elemental.json.JsonValue; */ @Connect(com.vaadin.ui.Grid.class) public class GridConnector extends AbstractHasComponentsConnector implements - SimpleManagedLayout { + SimpleManagedLayout, DeferredWorker { private static final class CustomCellStyleGenerator implements CellStyleGenerator<JsonObject> { @@ -167,7 +178,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements /** * Sets a new renderer for this column object - * + * * @param rendererConnector * a renderer connector object */ @@ -362,6 +373,305 @@ public class GridConnector extends AbstractHasComponentsConnector implements } } + private ColumnReorderHandler<JsonObject> columnReorderHandler = new ColumnReorderHandler<JsonObject>() { + + @Override + public void onColumnReorder(ColumnReorderEvent<JsonObject> event) { + if (!columnsUpdatedFromState) { + List<Column<?, JsonObject>> columns = getWidget().getColumns(); + final List<String> newColumnOrder = new ArrayList<String>(); + for (Column<?, JsonObject> column : columns) { + if (column instanceof CustomGridColumn) { + newColumnOrder.add(((CustomGridColumn) column).id); + } // the other case would be the multi selection column + } + getRpcProxy(GridServerRpc.class).columnsReordered( + newColumnOrder, columnOrder); + columnOrder = newColumnOrder; + getState().columnOrder = newColumnOrder; + } + } + }; + + private ColumnVisibilityChangeHandler<JsonObject> columnVisibilityChangeHandler = new ColumnVisibilityChangeHandler<JsonObject>() { + + @Override + public void onVisibilityChange( + ColumnVisibilityChangeEvent<JsonObject> event) { + if (!columnsUpdatedFromState) { + Column<?, JsonObject> column = event.getColumn(); + if (column instanceof CustomGridColumn) { + getRpcProxy(GridServerRpc.class).columnVisibilityChanged( + ((CustomGridColumn) column).id, column.isHidden(), + event.isUserOriginated()); + for (GridColumnState state : getState().columns) { + if (state.id.equals(((CustomGridColumn) column).id)) { + state.hidden = event.isHidden(); + break; + } + } + } else { + getLogger().warning( + "Visibility changed for a unknown column type in Grid: " + + column.toString() + ", type " + + column.getClass()); + } + } + } + }; + + private static class CustomDetailsGenerator implements DetailsGenerator { + + private final Map<Integer, ComponentConnector> indexToDetailsMap = new HashMap<Integer, ComponentConnector>(); + + @Override + @SuppressWarnings("boxing") + public Widget getDetails(int rowIndex) { + ComponentConnector componentConnector = indexToDetailsMap + .get(rowIndex); + if (componentConnector != null) { + return componentConnector.getWidget(); + } else { + return null; + } + } + + public void setDetailsConnectorChanges( + Set<DetailsConnectorChange> changes) { + /* + * To avoid overwriting connectors while moving them about, we'll + * take all the affected connectors, first all remove those that are + * removed or moved, then we add back those that are moved or added. + */ + + /* Remove moved/removed connectors from bookkeeping */ + for (DetailsConnectorChange change : changes) { + Integer oldIndex = change.getOldIndex(); + Connector removedConnector = indexToDetailsMap.remove(oldIndex); + + Connector connector = change.getConnector(); + assert removedConnector == null || connector == null + || removedConnector.equals(connector) : "Index " + + oldIndex + " points to " + removedConnector + + " while " + connector + " was expected"; + } + + /* Add moved/added connectors to bookkeeping */ + for (DetailsConnectorChange change : changes) { + Integer newIndex = change.getNewIndex(); + ComponentConnector connector = (ComponentConnector) change + .getConnector(); + + if (connector != null) { + assert newIndex != null : "An existing connector has a missing new index."; + + ComponentConnector prevConnector = indexToDetailsMap.put( + newIndex, connector); + + assert prevConnector == null : "Connector collision at index " + + newIndex + + " between old " + + prevConnector + + " and new " + connector; + } + } + } + } + + @SuppressWarnings("boxing") + private static class DetailsConnectorFetcher implements DeferredWorker { + + private static final int FETCH_TIMEOUT_MS = 5000; + + public interface Listener { + void fetchHasBeenScheduled(int id); + + void fetchHasReturned(int id); + } + + /** A flag making sure that we don't call scheduleFinally many times. */ + private boolean fetcherHasBeenCalled = false; + + /** A rolling counter for unique values. */ + private int detailsFetchCounter = 0; + + /** A collection that tracks the amount of requests currently underway. */ + private Set<Integer> pendingFetches = new HashSet<Integer>(5); + + private final ScheduledCommand lazyDetailsFetcher = new ScheduledCommand() { + @Override + public void execute() { + int currentFetchId = detailsFetchCounter++; + pendingFetches.add(currentFetchId); + rpc.sendDetailsComponents(currentFetchId); + fetcherHasBeenCalled = false; + + if (listener != null) { + listener.fetchHasBeenScheduled(currentFetchId); + } + + assert assertRequestDoesNotTimeout(currentFetchId); + } + }; + + private DetailsConnectorFetcher.Listener listener = null; + + private final GridServerRpc rpc; + + public DetailsConnectorFetcher(GridServerRpc rpc) { + assert rpc != null : "RPC was null"; + this.rpc = rpc; + } + + public void schedule() { + if (!fetcherHasBeenCalled) { + Scheduler.get().scheduleFinally(lazyDetailsFetcher); + fetcherHasBeenCalled = true; + } + } + + public void responseReceived(int fetchId) { + + if (fetchId < 0) { + /* Ignore negative fetchIds (they're pushed, not fetched) */ + return; + } + + boolean success = pendingFetches.remove(fetchId); + assert success : "Received a response with an unidentified fetch id"; + + if (listener != null) { + listener.fetchHasReturned(fetchId); + } + } + + @Override + public boolean isWorkPending() { + return fetcherHasBeenCalled || !pendingFetches.isEmpty(); + } + + private boolean assertRequestDoesNotTimeout(final int fetchId) { + /* + * This method will not be compiled without asserts enabled. This + * only makes sure that any request does not time out. + * + * TODO Should this be an explicit check? Is it worth the overhead? + */ + new Timer() { + @Override + public void run() { + assert !pendingFetches.contains(fetchId) : "Fetch id " + + fetchId + " timed out."; + } + }.schedule(FETCH_TIMEOUT_MS); + return true; + } + + public void setListener(DetailsConnectorFetcher.Listener listener) { + // if more are needed, feel free to convert this into a collection. + this.listener = listener; + } + } + + /** + * The functionality that makes sure that the scroll position is still kept + * up-to-date even if more details are being fetched lazily. + */ + private class LazyDetailsScrollAdjuster implements DeferredWorker { + + private static final int SCROLL_TO_END_ID = -2; + private static final int NO_SCROLL_SCHEDULED = -1; + + private class ScrollStopChecker implements DeferredWorker { + private final ScheduledCommand checkCommand = new ScheduledCommand() { + @Override + public void execute() { + isScheduled = false; + if (queuedFetches.isEmpty()) { + currentRow = NO_SCROLL_SCHEDULED; + destination = null; + } + } + }; + + private boolean isScheduled = false; + + public void schedule() { + if (isScheduled) { + return; + } + Scheduler.get().scheduleDeferred(checkCommand); + isScheduled = true; + } + + @Override + public boolean isWorkPending() { + return isScheduled; + } + } + + private DetailsConnectorFetcher.Listener fetcherListener = new DetailsConnectorFetcher.Listener() { + @Override + @SuppressWarnings("boxing") + public void fetchHasBeenScheduled(int id) { + if (currentRow != NO_SCROLL_SCHEDULED) { + queuedFetches.add(id); + } + } + + @Override + @SuppressWarnings("boxing") + public void fetchHasReturned(int id) { + if (currentRow == NO_SCROLL_SCHEDULED + || queuedFetches.isEmpty()) { + return; + } + + queuedFetches.remove(id); + if (currentRow == SCROLL_TO_END_ID) { + getWidget().scrollToEnd(); + } else { + getWidget().scrollToRow(currentRow, destination); + } + + /* + * Schedule a deferred call whether we should stop adjusting for + * scrolling. + * + * This is done deferredly just because we can't be absolutely + * certain whether this most recent scrolling won't cascade into + * further lazy details loading (perhaps deferredly). + */ + scrollStopChecker.schedule(); + } + }; + + private int currentRow = NO_SCROLL_SCHEDULED; + private final Set<Integer> queuedFetches = new HashSet<Integer>(); + private final ScrollStopChecker scrollStopChecker = new ScrollStopChecker(); + private ScrollDestination destination; + + public LazyDetailsScrollAdjuster() { + detailsConnectorFetcher.setListener(fetcherListener); + } + + public void adjustForEnd() { + currentRow = SCROLL_TO_END_ID; + } + + public void adjustFor(int row, ScrollDestination destination) { + currentRow = row; + this.destination = destination; + } + + @Override + public boolean isWorkPending() { + return currentRow != NO_SCROLL_SCHEDULED + || !queuedFetches.isEmpty() + || scrollStopChecker.isWorkPending(); + } + } + /** * Maps a generated column id to a grid column instance */ @@ -372,13 +682,22 @@ public class GridConnector extends AbstractHasComponentsConnector implements private List<String> columnOrder = new ArrayList<String>(); /** - * updateFromState is set to true when {@link #updateSelectionFromState()} - * makes changes to selection. This flag tells the - * {@code internalSelectionChangeHandler} to not send same data straight - * back to server. Said listener sets it back to false when handling that - * event. + * {@link #selectionUpdatedFromState} is set to true when + * {@link #updateSelectionFromState()} makes changes to selection. This flag + * tells the {@code internalSelectionChangeHandler} to not send same data + * straight back to server. Said listener sets it back to false when + * handling that event. + */ + private boolean selectionUpdatedFromState; + + /** + * {@link #columnsUpdatedFromState} is set to true when + * {@link #updateColumnOrderFromState(List)} is updating the column order + * for the widget. This flag tells the {@link #columnReorderHandler} to not + * send same data straight back to server. After updates, listener sets the + * value back to false. */ - private boolean updatedFromState = false; + private boolean columnsUpdatedFromState; private RpcDataSource dataSource; @@ -388,7 +707,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements if (event.isBatchedSelection()) { return; } - if (!updatedFromState) { + if (!selectionUpdatedFromState) { for (JsonObject row : event.getRemoved()) { selectedKeys.remove(dataSource.getRowKey(row)); } @@ -400,7 +719,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements getRpcProxy(GridServerRpc.class).select( new ArrayList<String>(selectedKeys)); } else { - updatedFromState = false; + selectionUpdatedFromState = false; } } }; @@ -409,6 +728,35 @@ public class GridConnector extends AbstractHasComponentsConnector implements private String lastKnownTheme = null; + private final CustomDetailsGenerator customDetailsGenerator = new CustomDetailsGenerator(); + + private final DetailsConnectorFetcher detailsConnectorFetcher = new DetailsConnectorFetcher( + getRpcProxy(GridServerRpc.class)); + + private final DetailsListener detailsListener = new DetailsListener() { + @Override + public void reapplyDetailsVisibility(int rowIndex, JsonObject row) { + if (hasDetailsOpen(row)) { + getWidget().setDetailsVisible(rowIndex, true); + detailsConnectorFetcher.schedule(); + } else { + getWidget().setDetailsVisible(rowIndex, false); + } + } + + private boolean hasDetailsOpen(JsonObject row) { + return row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE) + && row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE); + } + + @Override + public void closeDetails(int rowIndex) { + getWidget().setDetailsVisible(rowIndex, false); + } + }; + + private final LazyDetailsScrollAdjuster lazyDetailsScrollAdjuster = new LazyDetailsScrollAdjuster(); + @Override @SuppressWarnings("unchecked") public Grid<JsonObject> getWidget() { @@ -428,6 +776,10 @@ public class GridConnector extends AbstractHasComponentsConnector implements registerRpc(GridClientRpc.class, new GridClientRpc() { @Override public void scrollToStart() { + /* + * no need for lazyDetailsScrollAdjuster, because the start is + * always 0, won't change a bit. + */ Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { @@ -438,6 +790,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements @Override public void scrollToEnd() { + lazyDetailsScrollAdjuster.adjustForEnd(); Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { @@ -449,6 +802,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements @Override public void scrollToRow(final int row, final ScrollDestination destination) { + lazyDetailsScrollAdjuster.adjustFor(row, destination); Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { @@ -461,6 +815,51 @@ public class GridConnector extends AbstractHasComponentsConnector implements public void recalculateColumnWidths() { getWidget().recalculateColumnWidths(); } + + @Override + @SuppressWarnings("boxing") + public void setDetailsConnectorChanges( + Set<DetailsConnectorChange> connectorChanges, int fetchId) { + customDetailsGenerator + .setDetailsConnectorChanges(connectorChanges); + + List<DetailsConnectorChange> removedFirst = new ArrayList<DetailsConnectorChange>( + connectorChanges); + Collections.sort(removedFirst, + DetailsConnectorChange.REMOVED_FIRST_COMPARATOR); + + // refresh moved/added details rows + for (DetailsConnectorChange change : removedFirst) { + Integer oldIndex = change.getOldIndex(); + Integer newIndex = change.getNewIndex(); + + assert oldIndex == null || oldIndex >= 0 : "Got an " + + "invalid old index: " + oldIndex + + " (connector: " + change.getConnector() + ")"; + assert newIndex == null || newIndex >= 0 : "Got an " + + "invalid new index: " + newIndex + + " (connector: " + change.getConnector() + ")"; + + if (oldIndex != null) { + /* Close the old/removed index */ + getWidget().setDetailsVisible(oldIndex, false); + + if (change.isShouldStillBeVisible()) { + getWidget().setDetailsVisible(oldIndex, true); + } + } + + if (newIndex != null) { + /* + * Since the component was lazy loaded, we need to + * refresh the details by toggling it. + */ + getWidget().setDetailsVisible(newIndex, false); + getWidget().setDetailsVisible(newIndex, true); + } + } + detailsConnectorFetcher.responseReceived(fetchId); + } }); getWidget().addSelectionHandler(internalSelectionChangeHandler); @@ -503,7 +902,12 @@ public class GridConnector extends AbstractHasComponentsConnector implements }); getWidget().setEditorHandler(new CustomEditorHandler()); + getWidget().addColumnReorderHandler(columnReorderHandler); + getWidget().addColumnVisibilityChangeHandler( + columnVisibilityChangeHandler); + getWidget().setDetailsGenerator(customDetailsGenerator); getLayoutManager().registerDependency(this, getWidget().getElement()); + layout(); } @@ -522,7 +926,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements if (!columnIdToColumn.containsKey(state.id)) { addColumnFromStateChangeEvent(state); } - updateColumnFromState(columnIdToColumn.get(state.id), state); + updateColumnFromStateChangeEvent(state); } } @@ -596,7 +1000,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements columns[i] = columnIdToColumn.get(id); i++; } + columnsUpdatedFromState = true; getWidget().setColumnOrder(columns); + columnsUpdatedFromState = false; columnOrder = stateColumnOrder; } @@ -732,7 +1138,10 @@ public class GridConnector extends AbstractHasComponentsConnector implements */ private void updateColumnFromStateChangeEvent(GridColumnState columnState) { CustomGridColumn column = columnIdToColumn.get(columnState.id); + + columnsUpdatedFromState = true; updateColumnFromState(column, columnState); + columnsUpdatedFromState = false; } /** @@ -788,6 +1197,11 @@ public class GridConnector extends AbstractHasComponentsConnector implements column.setRenderer((AbstractRendererConnector<Object>) state.rendererConnector); column.setSortable(state.sortable); + + column.setHidden(state.hidden); + column.setHidable(state.hidable); + column.setHidingToggleCaption(state.hidingToggleCaption); + column.setEditable(state.editable); column.setEditorConnector((AbstractFieldConnector) state.editorConnector); } @@ -891,7 +1305,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements if (changed) { // At least for now there's no way to send the selected and/or // deselected row data. Some data is only stored as keys - updatedFromState = true; + selectionUpdatedFromState = true; getWidget().fireEvent( new SelectionEvent<JsonObject>(getWidget(), (List<JsonObject>) null, null, false)); @@ -994,4 +1408,14 @@ public class GridConnector extends AbstractHasComponentsConnector implements public void layout() { getWidget().onResize(); } + + @Override + public boolean isWorkPending() { + return detailsConnectorFetcher.isWorkPending() + || lazyDetailsScrollAdjuster.isWorkPending(); + } + + public DetailsListener getDetailsListener() { + return detailsListener; + } } diff --git a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java index f8d6ebcb62..627ee74eca 100644 --- a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java @@ -17,6 +17,7 @@ package com.vaadin.client.connectors; import java.util.ArrayList; +import java.util.List; import com.vaadin.client.ServerConnector; import com.vaadin.client.data.AbstractRemoteDataSource; @@ -43,6 +44,36 @@ import elemental.json.JsonObject; @Connect(com.vaadin.data.RpcDataProviderExtension.class) public class RpcDataSourceConnector extends AbstractExtensionConnector { + /** + * A callback interface to let {@link GridConnector} know that detail + * visibilities might have changed. + * + * @since 7.5.0 + * @author Vaadin Ltd + */ + interface DetailsListener { + + /** + * A request to verify (and correct) the visibility for a row, given + * updated metadata. + * + * @param rowIndex + * the index of the row that should be checked + * @param row + * the row object to check visibility for + * @see GridState#JSONKEY_DETAILS_VISIBLE + */ + void reapplyDetailsVisibility(int rowIndex, JsonObject row); + + /** + * Closes details for a row. + * + * @param rowIndex + * the index of the row for which to close details + */ + void closeDetails(int rowIndex); + } + public class RpcDataSource extends AbstractRemoteDataSource<JsonObject> { protected RpcDataSource() { @@ -56,27 +87,28 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { rows.add(rowObject); } - dataSource.setRowData(firstRow, rows); + RpcDataSource.this.setRowData(firstRow, rows); } @Override public void removeRowData(int firstRow, int count) { - dataSource.removeRowData(firstRow, count); + RpcDataSource.this.removeRowData(firstRow, count); } @Override public void insertRowData(int firstRow, int count) { - dataSource.insertRowData(firstRow, count); + RpcDataSource.this.insertRowData(firstRow, count); } @Override public void resetDataAndSize(int size) { - dataSource.resetDataAndSize(size); + RpcDataSource.this.resetDataAndSize(size); } }); } private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class); + private DetailsListener detailsListener; @Override protected void requestRows(int firstRowIndex, int numberOfRows, @@ -170,7 +202,29 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { if (!handle.isPinned()) { rpcProxy.setPinned(key, false); } + } + + void setDetailsListener(DetailsListener detailsListener) { + this.detailsListener = detailsListener; + } + + @Override + protected void setRowData(int firstRowIndex, List<JsonObject> rowData) { + super.setRowData(firstRowIndex, rowData); + /* + * Intercepting details information from the data source, rerouting + * them back to the GridConnector (as a details listener) + */ + for (int i = 0; i < rowData.size(); i++) { + detailsListener.reapplyDetailsVisibility(firstRowIndex + i, + rowData.get(i)); + } + } + + @Override + protected void onDropFromCache(int rowIndex) { + detailsListener.closeDetails(rowIndex); } } @@ -178,6 +232,8 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { @Override protected void extend(ServerConnector target) { - ((GridConnector) target).setDataSource(dataSource); + GridConnector gridConnector = (GridConnector) target; + dataSource.setDetailsListener(gridConnector.getDetailsListener()); + gridConnector.setDataSource(dataSource); } } diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index 1ab9764337..459127c9b4 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -332,9 +332,23 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { for (int i = range.getStart(); i < range.getEnd(); i++) { T removed = indexToRowMap.remove(Integer.valueOf(i)); keyToIndexMap.remove(getRowKey(removed)); + + onDropFromCache(i); } } + /** + * A hook that can be overridden to do something whenever a row is dropped + * from the cache. + * + * @since 7.5.0 + * @param rowIndex + * the index of the dropped row + */ + protected void onDropFromCache(int rowIndex) { + // noop + } + private void handleMissingRows(Range range) { if (range.isEmpty()) { return; @@ -574,6 +588,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { Profiler.leave("AbstractRemoteDataSource.insertRowData"); } + @SuppressWarnings("boxing") private void moveRowFromIndexToIndex(int oldIndex, int newIndex) { T row = indexToRowMap.remove(oldIndex); if (indexToRowMap.containsKey(newIndex)) { diff --git a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java new file mode 100644 index 0000000000..61fdd2850e --- /dev/null +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -0,0 +1,241 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.ui.dd; + +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.google.gwt.user.client.ui.RootPanel; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.widgets.Grid; + +/** + * A simple event handler for elements that can be drag and dropped. Properly + * handles drag start, cancel and end. For example, used in {@link Grid} column + * header reordering. + * <p> + * The showing of the dragged element, drag hints and reacting to drop/cancel is + * delegated to {@link DragAndDropCallback} implementation. + * + * @since + * @author Vaadin Ltd + */ +public class DragAndDropHandler { + + /** + * Callback interface for drag and drop. + */ + public interface DragAndDropCallback { + /** + * Called when the drag has started. The drag can be canceled by + * returning {@code false}. + * + * @param startEvent + * the original event that started the drag + * @return {@code true} if the drag is OK to start, {@code false} to + * cancel + */ + boolean onDragStart(NativeEvent startEvent); + + /** + * Called on drag. + * + * @param event + * the event related to the drag + */ + void onDragUpdate(NativePreviewEvent event); + + /** + * Called after the has ended on a drop or cancel. + */ + void onDragEnd(); + + /** + * Called when the drag has ended on a drop. + */ + void onDrop(); + + /** + * Called when the drag has been canceled. + */ + void onDragCancel(); + } + + private HandlerRegistration dragStartNativePreviewHandlerRegistration; + private HandlerRegistration dragHandlerRegistration; + + private boolean dragging; + + private DragAndDropCallback callback; + + private final NativePreviewHandler dragHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (dragging) { + final int typeInt = event.getTypeInt(); + switch (typeInt) { + case Event.ONKEYDOWN: + int keyCode = event.getNativeEvent().getKeyCode(); + if (keyCode == KeyCodes.KEY_ESCAPE) { + // end drag if ESC is hit + cancelDrag(event); + } + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + callback.onDragUpdate(event); + // prevent text selection on IE + event.getNativeEvent().preventDefault(); + break; + case Event.ONTOUCHCANCEL: + cancelDrag(event); + break; + case Event.ONTOUCHEND: + /* Avoid simulated event on drag end */ + event.getNativeEvent().preventDefault(); + //$FALL-THROUGH$ + case Event.ONMOUSEUP: + callback.onDragUpdate(event); + callback.onDrop(); + stopDrag(); + event.cancel(); + break; + default: + break; + } + } else { + stopDrag(); + } + } + + }; + + /** + * This method can be called to trigger drag and drop on any grid element + * that can be dragged and dropped. + * + * @param dragStartingEvent + * the drag triggering event, usually a {@link Event#ONMOUSEDOWN} + * or {@link Event#ONTOUCHSTART} event on the draggable element + * + * @param callback + * the callback that will handle actual drag and drop related + * operations + */ + public void onDragStartOnDraggableElement( + final NativeEvent dragStartingEvent, + final DragAndDropCallback callback) { + dragStartNativePreviewHandlerRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + private int startX = WidgetUtil + .getTouchOrMouseClientX(dragStartingEvent); + private int startY = WidgetUtil + .getTouchOrMouseClientY(dragStartingEvent); + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + final int typeInt = event.getTypeInt(); + if (typeInt == -1 + && event.getNativeEvent().getType() + .toLowerCase().contains("pointer")) { + /* + * Ignore PointerEvents since IE10 and IE11 send + * also MouseEvents for backwards compatibility. + */ + return; + } + switch (typeInt) { + case Event.ONMOUSEOVER: + case Event.ONMOUSEOUT: + // we don't care + break; + case Event.ONKEYDOWN: + case Event.ONKEYPRESS: + case Event.ONKEYUP: + case Event.ONBLUR: + case Event.ONFOCUS: + // don't cancel possible drag start + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + int currentX = WidgetUtil + .getTouchOrMouseClientX(event + .getNativeEvent()); + int currentY = WidgetUtil + .getTouchOrMouseClientY(event + .getNativeEvent()); + if (Math.abs(startX - currentX) > 3 + || Math.abs(startY - currentY) > 3) { + removeNativePreviewHandlerRegistration(); + startDrag(dragStartingEvent, event, callback); + } + break; + default: + // on any other events, clean up this preview + // listener + removeNativePreviewHandlerRegistration(); + break; + } + } + }); + } + + private void startDrag(NativeEvent startEvent, + NativePreviewEvent triggerEvent, DragAndDropCallback callback) { + if (callback.onDragStart(startEvent)) { + dragging = true; + // just capture something to prevent text selection in IE + Event.setCapture(RootPanel.getBodyElement()); + this.callback = callback; + dragHandlerRegistration = Event + .addNativePreviewHandler(dragHandler); + callback.onDragUpdate(triggerEvent); + } + } + + private void stopDrag() { + dragging = false; + if (dragHandlerRegistration != null) { + dragHandlerRegistration.removeHandler(); + dragHandlerRegistration = null; + } + Event.releaseCapture(RootPanel.getBodyElement()); + if (callback != null) { + callback.onDragEnd(); + callback = null; + } + } + + private void cancelDrag(NativePreviewEvent event) { + callback.onDragCancel(); + callback.onDragEnd(); + stopDrag(); + event.cancel(); + event.getNativeEvent().preventDefault(); + } + + private void removeNativePreviewHandlerRegistration() { + if (dragStartNativePreviewHandlerRegistration != null) { + dragStartNativePreviewHandlerRegistration.removeHandler(); + dragStartNativePreviewHandlerRegistration = null; + } + } +} diff --git a/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java b/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java index 6109c5e51d..54507a7650 100644 --- a/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java +++ b/client/src/com/vaadin/client/widget/escalator/EscalatorUpdater.java @@ -16,8 +16,6 @@ package com.vaadin.client.widget.escalator; -import com.vaadin.client.widgets.Escalator; - /** * An interface that allows client code to define how a certain row in Escalator * will be displayed. The contents of an escalator's header, body and footer are diff --git a/client/src/com/vaadin/client/widget/escalator/Row.java b/client/src/com/vaadin/client/widget/escalator/Row.java index bcb3e163e4..fa89853120 100644 --- a/client/src/com/vaadin/client/widget/escalator/Row.java +++ b/client/src/com/vaadin/client/widget/escalator/Row.java @@ -17,7 +17,6 @@ package com.vaadin.client.widget.escalator; import com.google.gwt.dom.client.TableRowElement; -import com.vaadin.client.widgets.Escalator; /** * A representation of a row in an {@link Escalator}. diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java index 397336450e..abab25046c 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -22,17 +22,102 @@ import com.google.gwt.dom.client.TableSectionElement; /** * A representation of the rows in each of the sections (header, body and - * footer) in an {@link Escalator}. + * footer) in an {@link com.vaadin.client.widgets.Escalator}. * * @since 7.4 * @author Vaadin Ltd - * @see Escalator#getHeader() - * @see Escalator#getBody() - * @see Escalator#getFooter() + * @see com.vaadin.client.widgets.Escalator#getHeader() + * @see com.vaadin.client.widgets.Escalator#getBody() + * @see com.vaadin.client.widgets.Escalator#getFooter() + * @see SpacerContainer */ public interface RowContainer { /** + * The row container for the body section in an + * {@link com.vaadin.client.widgets.Escalator}. + * <p> + * The body section can contain both rows and spacers. + * + * @since 7.5.0 + * @author Vaadin Ltd + * @see com.vaadin.client.widgets.Escalator#getBody() + */ + public interface BodyRowContainer extends RowContainer { + + /** + * Marks a spacer and its height. + * <p> + * If a spacer is already registered with the given row index, that + * spacer will be updated with the given height. + * <p> + * <em>Note:</em> The row index for a spacer will change if rows are + * inserted or removed above the current position. Spacers will also be + * removed alongside their associated rows + * + * @param rowIndex + * the row index for the spacer to modify. The affected + * spacer is underneath the given index. Use -1 to insert a + * spacer before the first row + * @param height + * the pixel height of the spacer. If {@code height} is + * negative, the affected spacer (if exists) will be removed + * @throws IllegalArgumentException + * if {@code rowIndex} is not a valid row index + * @see #insertRows(int, int) + * @see #removeRows(int, int) + */ + void setSpacer(int rowIndex, double height) + throws IllegalArgumentException; + + /** + * Sets a new spacer updater. + * <p> + * Spacers that are currently visible will be updated, i.e. + * {@link SpacerUpdater#destroy(Spacer) destroyed} with the previous + * one, and {@link SpacerUpdater#init(Spacer) initialized} with the new + * one. + * + * @param spacerUpdater + * the new spacer updater + * @throws IllegalArgumentException + * if {@code spacerUpdater} is {@code null} + */ + void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException; + + /** + * Gets the spacer updater currently in use. + * <p> + * {@link SpacerUpdater#NULL} is the default. + * + * @return the spacer updater currently in use. Never <code>null</code> + */ + SpacerUpdater getSpacerUpdater(); + + /** + * {@inheritDoc} + * <p> + * Any spacers underneath {@code index} will be offset and "pushed" + * down. This also modifies the row index they are associated with. + */ + @Override + public void insertRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * {@inheritDoc} + * <p> + * Any spacers underneath {@code index} will be offset and "pulled" up. + * This also modifies the row index they are associated with. Any + * spacers in the removed range will also be closed and removed. + */ + @Override + public void removeRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + } + + /** * An arbitrary pixel height of a row, before any autodetection for the row * height has been made. * */ diff --git a/client/src/com/vaadin/client/widget/escalator/Spacer.java b/client/src/com/vaadin/client/widget/escalator/Spacer.java new file mode 100644 index 0000000000..789a64a21e --- /dev/null +++ b/client/src/com/vaadin/client/widget/escalator/Spacer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.escalator; + +import com.google.gwt.dom.client.Element; + +/** + * A representation of a spacer element in a + * {@link com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer}. + * + * @since 7.5.0 + * @author Vaadin Ltd + */ +public interface Spacer { + + /** + * Gets the root element for the spacer content. + * + * @return the root element for the spacer content + */ + Element getElement(); + + /** + * Gets the decorative element for this spacer. + */ + Element getDecoElement(); + + /** + * Gets the row index. + * + * @return the row index. + */ + int getRow(); +} diff --git a/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java new file mode 100644 index 0000000000..49adefd536 --- /dev/null +++ b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.escalator; + +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; + +/** + * An interface that handles the display of content for spacers. + * <p> + * The updater is responsible for making sure all elements are properly + * constructed and cleaned up. + * + * @since 7.5.0 + * @author Vaadin Ltd + * @see Spacer + * @see BodyRowContainer + */ +public interface SpacerUpdater { + + /** A spacer updater that does nothing. */ + public static final SpacerUpdater NULL = new SpacerUpdater() { + @Override + public void init(Spacer spacer) { + // NOOP + } + + @Override + public void destroy(Spacer spacer) { + // NOOP + } + }; + + /** + * Called whenever a spacer should be initialized with content. + * + * @param spacer + * the spacer reference that should be initialized + */ + void init(Spacer spacer); + + /** + * Called whenever a spacer should be cleaned. + * <p> + * The structure to clean up is the same that has been constructed by + * {@link #init(Spacer)}. + * + * @param spacer + * the spacer reference that should be destroyed + */ + void destroy(Spacer spacer); +} diff --git a/client/src/com/vaadin/client/widget/grid/AutoScroller.java b/client/src/com/vaadin/client/widget/grid/AutoScroller.java new file mode 100644 index 0000000000..f2e44196ec --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -0,0 +1,689 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid; + +import com.google.gwt.animation.client.AnimationScheduler; +import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; +import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.TableElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.widgets.Grid; + +/** + * A class for handling automatic scrolling vertically / horizontally in the + * Grid when the cursor is close enough the edge of the body of the grid, + * depending on the scroll direction chosen. + * + * @since 7.5.0 + * @author Vaadin Ltd + */ +public class AutoScroller { + + /** + * Callback that notifies when the cursor is on top of a new row or column + * because of the automatic scrolling. + */ + public interface AutoScrollerCallback { + + /** + * Triggered when doing automatic scrolling. + * <p> + * Because the auto scroller currently only supports scrolling in one + * axis, this method is used for both vertical and horizontal scrolling. + * + * @param scrollDiff + * the amount of pixels that have been auto scrolled since + * last call + */ + void onAutoScroll(int scrollDiff); + + /** + * Triggered when the grid scroll has reached the minimum scroll + * position. Depending on the scroll axis, either scrollLeft or + * scrollTop is 0. + */ + void onAutoScrollReachedMin(); + + /** + * Triggered when the grid scroll has reached the max scroll position. + * Depending on the scroll axis, either scrollLeft or scrollTop is at + * its maximum value. + */ + void onAutoScrollReachedMax(); + } + + public enum ScrollAxis { + VERTICAL, HORIZONTAL + } + + /** The maximum number of pixels per second to autoscroll. */ + private static final int SCROLL_TOP_SPEED_PX_SEC = 500; + + /** + * The minimum area where the grid doesn't scroll while the pointer is + * pressed. + */ + private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50; + + /** The size of the autoscroll area, both top/left and bottom/right. */ + private int scrollAreaPX = 100; + + /** + * This class's main objective is to listen when to stop autoscrolling, and + * make sure everything stops accordingly. + */ + private class TouchEventHandler implements NativePreviewHandler { + @Override + public void onPreviewNativeEvent(final NativePreviewEvent event) { + /* + * Remember: targetElement is always where touchstart started, not + * where the finger is pointing currently. + */ + switch (event.getTypeInt()) { + case Event.ONTOUCHSTART: { + if (event.getNativeEvent().getTouches().length() == 1) { + /* + * Something has dropped a touchend/touchcancel and the + * scroller is most probably running amok. Let's cancel it + * and pretend that everything's going as expected + * + * Because this is a preview, this code is run before start + * event can be passed to the start(...) method. + */ + stop(); + + /* + * Related TODO: investigate why iOS seems to ignore a + * touchend/touchcancel when frames are dropped, and/or if + * something can be done about that. + */ + } + break; + } + + case Event.ONTOUCHMOVE: + event.cancel(); + break; + + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + // TODO investigate if this works as desired + stop(); + break; + } + } + + } + + /** + * This class's responsibility is to scroll the table while a pointer is + * kept in a scrolling zone. + * <p> + * <em>Techical note:</em> This class is an AnimationCallback because we + * need a timer: when the finger is kept in place while the grid scrolls, we + * still need to be able to make new selections. So, instead of relying on + * events (which won't be fired, since the pointer isn't necessarily + * moving), we do this check on each frame while the pointer is "active" + * (mouse is pressed, finger is on screen). + */ + private class AutoScrollingFrame implements AnimationCallback { + + /** + * If the acceleration gradient area is smaller than this, autoscrolling + * will be disabled (it becomes too quick to accelerate to be usable). + */ + private static final int GRADIENT_MIN_THRESHOLD_PX = 10; + + /** + * The speed at which the gradient area recovers, once scrolling in that + * direction has started. + */ + private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1; + private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC / 1000.0d; + + /** + * The lowest y/x-coordinate on the {@link Event#getClientY() client-y} + * or {@link Event#getClientX() client-x} from where we need to start + * scrolling towards the top/left. + */ + private int startBound = -1; + + /** + * The highest y/x-coordinate on the {@link Event#getClientY() client-y} + * or {@link Event#getClientX() client-x} from where we need to + * scrolling towards the bottom. + */ + private int endBound = -1; + + /** + * The area where the selection acceleration takes place. If < + * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled + */ + private final int gradientArea; + + /** + * The number of pixels per seconds we currently are scrolling (negative + * is towards the top/left, positive is towards the bottom/right). + */ + private double scrollSpeed = 0; + + private double prevTimestamp = 0; + + /** + * This field stores fractions of pixels to scroll, to make sure that + * we're able to scroll less than one px per frame. + */ + private double pixelsToScroll = 0.0d; + + /** Should this animator be running. */ + private boolean running = false; + + /** The handle in which this instance is running. */ + private AnimationHandle handle; + + /** + * The pointer's pageY (VERTICAL) / pageX (HORIZONTAL) coordinate + * depending on scrolling axis. + */ + private int scrollingAxisPageCoordinate; + + /** @see #doScrollAreaChecks(int) */ + private int finalStartBound; + + /** @see #doScrollAreaChecks(int) */ + private int finalEndBound; + + private boolean scrollAreaShouldRebound = false; + + public AutoScrollingFrame(final int startBound, final int endBound, + final int gradientArea) { + finalStartBound = startBound; + finalEndBound = endBound; + this.gradientArea = gradientArea; + } + + @Override + public void execute(final double timestamp) { + final double timeDiff = timestamp - prevTimestamp; + prevTimestamp = timestamp; + + reboundScrollArea(timeDiff); + + pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d); + final int intPixelsToScroll = (int) pixelsToScroll; + pixelsToScroll -= intPixelsToScroll; + + if (intPixelsToScroll != 0) { + double scrollPos; + double maxScrollPos; + double newScrollPos; + if (scrollDirection == ScrollAxis.VERTICAL) { + scrollPos = grid.getScrollTop(); + maxScrollPos = getMaxScrollTop(); + } else { + scrollPos = grid.getScrollLeft(); + maxScrollPos = getMaxScrollLeft(); + } + if (intPixelsToScroll > 0 && scrollPos < maxScrollPos + || intPixelsToScroll < 0 && scrollPos > 0) { + newScrollPos = scrollPos + intPixelsToScroll; + if (scrollDirection == ScrollAxis.VERTICAL) { + grid.setScrollTop(newScrollPos); + } else { + grid.setScrollLeft(newScrollPos); + } + callback.onAutoScroll(intPixelsToScroll); + if (newScrollPos <= 0) { + callback.onAutoScrollReachedMin(); + } else if (newScrollPos >= maxScrollPos) { + callback.onAutoScrollReachedMax(); + } + } + } + + reschedule(); + } + + /** + * If the scroll are has been offset by the pointer starting out there, + * move it back a bit + */ + private void reboundScrollArea(double timeDiff) { + if (!scrollAreaShouldRebound) { + return; + } + + int reboundPx = (int) Math.ceil(SCROLL_AREA_REBOUND_PX_PER_MS + * timeDiff); + if (startBound < finalStartBound) { + startBound += reboundPx; + startBound = Math.min(startBound, finalStartBound); + updateScrollSpeed(scrollingAxisPageCoordinate); + } else if (endBound > finalEndBound) { + endBound -= reboundPx; + endBound = Math.max(endBound, finalEndBound); + updateScrollSpeed(scrollingAxisPageCoordinate); + } + } + + private void updateScrollSpeed(final int pointerPageCordinate) { + + final double ratio; + if (pointerPageCordinate < startBound) { + final double distance = pointerPageCordinate - startBound; + ratio = Math.max(-1, distance / gradientArea); + } + + else if (pointerPageCordinate > endBound) { + final double distance = pointerPageCordinate - endBound; + ratio = Math.min(1, distance / gradientArea); + } + + else { + ratio = 0; + } + + scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC; + } + + public void start() { + running = true; + reschedule(); + } + + public void stop() { + running = false; + + if (handle != null) { + handle.cancel(); + handle = null; + } + } + + private void reschedule() { + if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) { + handle = AnimationScheduler.get().requestAnimationFrame(this, + grid.getElement()); + } + } + + public void updatePointerCoords(int pageX, int pageY) { + final int pageCordinate; + if (scrollDirection == ScrollAxis.VERTICAL) { + pageCordinate = pageY; + } else { + pageCordinate = pageX; + } + doScrollAreaChecks(pageCordinate); + updateScrollSpeed(pageCordinate); + scrollingAxisPageCoordinate = pageCordinate; + } + + /** + * This method checks whether the first pointer event started in an area + * that would start scrolling immediately, and does some actions + * accordingly. + * <p> + * If it is, that scroll area will be offset "beyond" the pointer (above + * if pointer is towards the top/left, otherwise below/right). + */ + private void doScrollAreaChecks(int pageCordinate) { + /* + * The first run makes sure that neither scroll position is + * underneath the finger, but offset to either direction from + * underneath the pointer. + */ + if (startBound == -1) { + startBound = Math.min(finalStartBound, pageCordinate); + endBound = Math.max(finalEndBound, pageCordinate); + } + + /* + * Subsequent runs make sure that the scroll area grows (but doesn't + * shrink) with the finger, but no further than the final bound. + */ + else { + int oldTopBound = startBound; + if (startBound < finalStartBound) { + startBound = Math.max(startBound, + Math.min(finalStartBound, pageCordinate)); + } + + int oldBottomBound = endBound; + if (endBound > finalEndBound) { + endBound = Math.min(endBound, + Math.max(finalEndBound, pageCordinate)); + } + + final boolean startDidNotMove = oldTopBound == startBound; + final boolean endDidNotMove = oldBottomBound == endBound; + final boolean wasMovement = pageCordinate != scrollingAxisPageCoordinate; + scrollAreaShouldRebound = (startDidNotMove && endDidNotMove && wasMovement); + } + } + } + + /** + * This handler makes sure that pointer movements are handled. + * <p> + * Essentially, a native preview handler is registered (so that selection + * gestures can happen outside of the selection column). The handler itself + * makes sure that it's detached when the pointer is "lifted". + */ + private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() { + @Override + public void onPreviewNativeEvent(final NativePreviewEvent event) { + if (autoScroller == null) { + stop(); + return; + } + + final NativeEvent nativeEvent = event.getNativeEvent(); + int pageY = 0; + int pageX = 0; + switch (event.getTypeInt()) { + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent); + pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent); + autoScroller.updatePointerCoords(pageX, pageY); + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + stop(); + break; + } + } + }; + /** The registration info for {@link #scrollPreviewHandler} */ + private HandlerRegistration handlerRegistration; + + /** + * The top/left bound, as calculated from the {@link Event#getClientY() + * client-y} or {@link Event#getClientX() client-x} coordinates. + */ + private double startingBound = -1; + + /** + * The bottom/right bound, as calculated from the {@link Event#getClientY() + * client-y} or or {@link Event#getClientX() client-x} coordinates. + */ + private int endingBound = -1; + + /** The size of the autoscroll acceleration area. */ + private int gradientArea; + + private Grid<?> grid; + + private HandlerRegistration nativePreviewHandlerRegistration; + + private ScrollAxis scrollDirection; + + private AutoScrollingFrame autoScroller; + + private AutoScrollerCallback callback; + + /** + * Creates a new instance for scrolling the given grid. + * + * @param grid + * the grid to auto scroll + */ + public AutoScroller(Grid<?> grid) { + this.grid = grid; + } + + /** + * Starts the automatic scrolling detection. + * + * @param startEvent + * the event that starts the automatic scroll + * @param scrollAxis + * the axis along which the scrolling should happen + * @param callback + * the callback for getting info about the automatic scrolling + */ + public void start(final NativeEvent startEvent, ScrollAxis scrollAxis, + AutoScrollerCallback callback) { + scrollDirection = scrollAxis; + this.callback = callback; + injectNativeHandler(); + start(); + startEvent.preventDefault(); + startEvent.stopPropagation(); + } + + /** + * Stops the automatic scrolling. + */ + public void stop() { + if (handlerRegistration != null) { + handlerRegistration.removeHandler(); + handlerRegistration = null; + } + + if (autoScroller != null) { + autoScroller.stop(); + autoScroller = null; + } + + removeNativeHandler(); + } + + /** + * Set the auto scroll area height or width depending on the scrolling axis. + * This is the amount of pixels from the edge of the grid that the scroll is + * triggered. + * <p> + * Defaults to 100px. + * + * @param px + * the pixel height/width for the auto scroll area depending on + * direction + */ + public void setScrollArea(int px) { + scrollAreaPX = px; + } + + /** + * Returns the size of the auto scroll area in pixels. + * <p> + * Defaults to 100px. + * + * @return size in pixels + */ + public int getScrollArea() { + return scrollAreaPX; + } + + private void start() { + /* + * bounds are updated whenever the autoscroll cycle starts, to make sure + * that the widget hasn't changed in size, moved around, or whatnot. + */ + updateScrollBounds(); + + assert handlerRegistration == null : "handlerRegistration was not null"; + assert autoScroller == null : "autoScroller was not null"; + handlerRegistration = Event + .addNativePreviewHandler(scrollPreviewHandler); + autoScroller = new AutoScrollingFrame((int) Math.ceil(startingBound), + endingBound, gradientArea); + autoScroller.start(); + } + + private void updateScrollBounds() { + double startBorder = getBodyClientStart(); + final int endBorder = getBodyClientEnd(); + startBorder += getFrozenColumnsWidth(); + + final int scrollCompensation = getScrollCompensation(); + startingBound = scrollCompensation + startBorder + scrollAreaPX; + endingBound = scrollCompensation + endBorder - scrollAreaPX; + gradientArea = scrollAreaPX; + + // modify bounds if they're too tightly packed + if (endingBound - startingBound < MIN_NO_AUTOSCROLL_AREA_PX) { + double adjustment = MIN_NO_AUTOSCROLL_AREA_PX + - (endingBound - startingBound); + startingBound -= adjustment / 2; + endingBound += adjustment / 2; + gradientArea -= adjustment / 2; + } + } + + private int getScrollCompensation() { + Element cursor = grid.getElement(); + int scroll = 0; + while (cursor != null) { + scroll -= scrollDirection == ScrollAxis.VERTICAL ? cursor + .getScrollTop() : cursor.getScrollLeft(); + cursor = cursor.getParentElement(); + } + + return scroll; + } + + private void injectNativeHandler() { + removeNativeHandler(); + nativePreviewHandlerRegistration = Event + .addNativePreviewHandler(new TouchEventHandler()); + } + + private void removeNativeHandler() { + if (nativePreviewHandlerRegistration != null) { + nativePreviewHandlerRegistration.removeHandler(); + nativePreviewHandlerRegistration = null; + } + } + + private TableElement getTableElement() { + final Element root = grid.getElement(); + final Element tablewrapper = Element.as(root.getChild(2)); + if (tablewrapper != null) { + return TableElement.as(tablewrapper.getFirstChildElement()); + } else { + return null; + } + } + + private TableSectionElement getTbodyElement() { + TableElement table = getTableElement(); + if (table != null) { + return table.getTBodies().getItem(0); + } else { + return null; + } + } + + private TableSectionElement getTheadElement() { + TableElement table = getTableElement(); + if (table != null) { + return table.getTHead(); + } else { + return null; + } + } + + private TableSectionElement getTfootElement() { + TableElement table = getTableElement(); + if (table != null) { + return table.getTFoot(); + } else { + return null; + } + } + + /** Get the "top" of an element in relation to "client" coordinates. */ + @SuppressWarnings("static-method") + private int getClientTop(final Element e) { + Element cursor = e; + int top = 0; + while (cursor != null) { + top += cursor.getOffsetTop(); + cursor = cursor.getOffsetParent(); + } + return top; + } + + /** Get the "left" of an element in relation to "client" coordinates. */ + @SuppressWarnings("static-method") + private int getClientLeft(final Element e) { + Element cursor = e; + int left = 0; + while (cursor != null) { + left += cursor.getOffsetLeft(); + cursor = cursor.getOffsetParent(); + } + return left; + } + + private int getBodyClientEnd() { + if (scrollDirection == ScrollAxis.VERTICAL) { + return getClientTop(getTfootElement()) - 1; + } else { + TableSectionElement tbodyElement = getTbodyElement(); + return getClientLeft(tbodyElement) + tbodyElement.getOffsetWidth() + - 1; + } + + } + + private int getBodyClientStart() { + if (scrollDirection == ScrollAxis.VERTICAL) { + return getClientTop(grid.getElement()) + + getTheadElement().getOffsetHeight(); + } else { + return getClientLeft(getTbodyElement()); + } + } + + private double getFrozenColumnsWidth() { + double value = getMultiSelectColumnWidth(); + for (int i = 0; i < grid.getFrozenColumnCount(); i++) { + value += grid.getColumn(i).getWidthActual(); + } + return value; + } + + private double getMultiSelectColumnWidth() { + if (grid.getFrozenColumnCount() >= 0 + && grid.getSelectionModel().getSelectionColumnRenderer() != null) { + // frozen checkbox column is present + return getTheadElement().getFirstChildElement() + .getFirstChildElement().getOffsetWidth(); + } + return 0.0; + } + + private double getMaxScrollLeft() { + return grid.getScrollWidth() + - (getTableElement().getParentElement().getOffsetWidth() - getFrozenColumnsWidth()); + } + + private double getMaxScrollTop() { + return grid.getScrollHeight() - getTfootElement().getOffsetHeight() + - getTheadElement().getOffsetHeight(); + } +} diff --git a/client/src/com/vaadin/client/widget/grid/CellReference.java b/client/src/com/vaadin/client/widget/grid/CellReference.java index a2e841de43..e783cb92ae 100644 --- a/client/src/com/vaadin/client/widget/grid/CellReference.java +++ b/client/src/com/vaadin/client/widget/grid/CellReference.java @@ -32,6 +32,8 @@ import com.vaadin.client.widgets.Grid; * @since 7.4 */ public class CellReference<T> { + + private int columnIndexDOM; private int columnIndex; private Grid.Column<?, T> column; private final RowReference<T> rowReference; @@ -42,13 +44,20 @@ public class CellReference<T> { /** * Sets the identifying information for this cell. + * <p> + * The difference between {@link #columnIndexDOM} and {@link #columnIndex} + * comes from hidden columns. * + * @param columnIndexDOM + * the index of the column in the DOM * @param columnIndex * the index of the column * @param column * the column object */ - public void set(int columnIndex, Grid.Column<?, T> column) { + public void set(int columnIndexDOM, int columnIndex, + Grid.Column<?, T> column) { + this.columnIndexDOM = columnIndexDOM; this.columnIndex = columnIndex; this.column = column; } @@ -82,6 +91,9 @@ public class CellReference<T> { /** * Gets the index of the column. + * <p> + * <em>NOTE:</em> The index includes hidden columns in the count, unlike + * {@link #getColumnIndexDOM()}. * * @return the index of the column */ @@ -90,6 +102,17 @@ public class CellReference<T> { } /** + * Gets the index of the cell in the DOM. The difference to + * {@link #getColumnIndex()} is caused by hidden columns. + * + * @since 7.5.0 + * @return the index of the column in the DOM + */ + public int getColumnIndexDOM() { + return columnIndexDOM; + } + + /** * Gets the column objects. * * @return the column object @@ -113,7 +136,7 @@ public class CellReference<T> { * @return the element of the cell */ public TableCellElement getElement() { - return rowReference.getElement().getCells().getItem(columnIndex); + return rowReference.getElement().getCells().getItem(columnIndexDOM); } /** diff --git a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java new file mode 100644 index 0000000000..b9427091a7 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid; + +import com.google.gwt.user.client.ui.Widget; + +/** + * A callback interface for generating details for a particular row in Grid. + * + * @since 7.5.0 + * @author Vaadin Ltd + */ +public interface DetailsGenerator { + + /** A details generator that provides no details */ + public static final DetailsGenerator NULL = new DetailsGenerator() { + @Override + public Widget getDetails(int rowIndex) { + return null; + } + }; + + /** + * This method is called for whenever a new details row needs to be + * generated. + * + * @param rowIndex + * the index of the row for which to generate details + * @return the details for the given row, or <code>null</code> to leave the + * details empty. + */ + Widget getDetails(int rowIndex); +} diff --git a/client/src/com/vaadin/client/widget/grid/EventCellReference.java b/client/src/com/vaadin/client/widget/grid/EventCellReference.java index cf13798e11..7ca1d5de75 100644 --- a/client/src/com/vaadin/client/widget/grid/EventCellReference.java +++ b/client/src/com/vaadin/client/widget/grid/EventCellReference.java @@ -18,6 +18,7 @@ package com.vaadin.client.widget.grid; import com.google.gwt.dom.client.TableCellElement; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widgets.Grid; +import com.vaadin.client.widgets.Grid.Column; /** * A data class which contains information which identifies a cell being the @@ -48,11 +49,14 @@ public class EventCellReference<T> extends CellReference<T> { */ public void set(Cell targetCell) { int row = targetCell.getRow(); - int column = targetCell.getColumn(); + int columnIndexDOM = targetCell.getColumn(); + Column<?, T> column = grid.getVisibleColumns().get(columnIndexDOM); + // At least for now we don't need to have the actual TableRowElement // available. getRowReference().set(row, grid.getDataSource().getRow(row), null); - set(column, grid.getColumn(column)); + int columnIndex = grid.getColumns().indexOf(column); + set(columnIndexDOM, columnIndex, column); this.element = targetCell.getElement(); } diff --git a/client/src/com/vaadin/client/widget/grid/RendererCellReference.java b/client/src/com/vaadin/client/widget/grid/RendererCellReference.java index 533eafded6..994db50aa0 100644 --- a/client/src/com/vaadin/client/widget/grid/RendererCellReference.java +++ b/client/src/com/vaadin/client/widget/grid/RendererCellReference.java @@ -49,12 +49,16 @@ public class RendererCellReference extends CellReference<Object> { * * @param cell * the flyweight cell to reference + * @param columnIndex + * the index of the column in the grid, including hidden cells * @param column * the column to reference */ - public void set(FlyweightCell cell, Grid.Column<?, ?> column) { + public void set(FlyweightCell cell, int columnIndex, + Grid.Column<?, ?> column) { this.cell = cell; - super.set(cell.getColumn(), (Grid.Column<?, Object>) column); + super.set(cell.getColumn(), columnIndex, + (Grid.Column<?, Object>) column); } /** diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java new file mode 100644 index 0000000000..1712871089 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.GwtEvent; + +/** + * An event for notifying that the columns in the Grid have been reordered. + * + * @param <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since 7.5.0 + * @author Vaadin Ltd + */ +public class ColumnReorderEvent<T> extends GwtEvent<ColumnReorderHandler<T>> { + + /** + * Handler type. + */ + private final static Type<ColumnReorderHandler<?>> TYPE = new Type<ColumnReorderHandler<?>>(); + + public static final Type<ColumnReorderHandler<?>> getType() { + return TYPE; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Type<ColumnReorderHandler<T>> getAssociatedType() { + return (Type) TYPE; + } + + @Override + protected void dispatch(ColumnReorderHandler<T> handler) { + handler.onColumnReorder(this); + } + +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java new file mode 100644 index 0000000000..29c476058e --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for a Grid column reorder event, called when the Grid's columns has + * been reordered. + * + * @param <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since 7.5.0 + * @author Vaadin Ltd + */ +public interface ColumnReorderHandler<T> extends EventHandler { + + /** + * A column reorder event, fired by Grid when the columns of the Grid have + * been reordered. + * + * @param event + * column reorder event + */ + public void onColumnReorder(ColumnReorderEvent<T> event); +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java new file mode 100644 index 0000000000..63b788bcf2 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.GwtEvent; +import com.vaadin.client.widgets.Grid.Column; + +/** + * An event for notifying that the columns in the Grid's have changed + * visibility. + * + * @param <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since 7.5.0 + * @author Vaadin Ltd + */ +public class ColumnVisibilityChangeEvent<T> extends + GwtEvent<ColumnVisibilityChangeHandler<T>> { + + private final static Type<ColumnVisibilityChangeHandler<?>> TYPE = new Type<ColumnVisibilityChangeHandler<?>>(); + + public static final Type<ColumnVisibilityChangeHandler<?>> getType() { + return TYPE; + } + + private final Column<?, T> column; + + private final boolean userOriginated; + + private final boolean hidden; + + public ColumnVisibilityChangeEvent(Column<?, T> column, boolean hidden, + boolean userOriginated) { + this.column = column; + this.hidden = hidden; + this.userOriginated = userOriginated; + } + + /** + * Returns the column where the visibility change occurred. + * + * @return the column where the visibility change occurred. + */ + public Column<?, T> getColumn() { + return column; + } + + /** + * Was the column set hidden or visible. + * + * @return <code>true</code> if the column was hidden <code>false</code> if + * it was set visible + */ + public boolean isHidden() { + return hidden; + } + + /** + * Is the visibility change triggered by user. + * + * @return <code>true</code> if the change was triggered by user, + * <code>false</code> if not + */ + public boolean isUserOriginated() { + return userOriginated; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public com.google.gwt.event.shared.GwtEvent.Type<ColumnVisibilityChangeHandler<T>> getAssociatedType() { + return (Type) TYPE; + } + + @Override + protected void dispatch(ColumnVisibilityChangeHandler<T> handler) { + handler.onVisibilityChange(this); + } + +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java new file mode 100644 index 0000000000..542fe4e3c1 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for a Grid column visibility change event, called when the Grid's + * columns have changed visibility to hidden or visible. + * + * @param<T> The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since 7.5.0 + * @author Vaadin Ltd + */ +public interface ColumnVisibilityChangeHandler<T> extends EventHandler { + + /** + * A column visibility change event, fired by Grid when a column in the Grid + * has changed visibility. + * + * @param event + * column visibility change event + */ + public void onVisibilityChange(ColumnVisibilityChangeEvent<T> event); +} diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 3d4459a0cc..0cd59ce7ed 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -16,6 +16,8 @@ package com.vaadin.client.widgets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -23,6 +25,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -51,12 +54,14 @@ import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.DeferredWorker; import com.vaadin.client.Profiler; import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.SubPartAware; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; @@ -68,11 +73,14 @@ import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition; import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition; import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.widget.escalator.RowContainer; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.ScrollbarBundle; import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.client.widget.escalator.Spacer; +import com.vaadin.client.widget.escalator.SpacerUpdater; import com.vaadin.client.widget.grid.events.ScrollEvent; import com.vaadin.client.widget.grid.events.ScrollHandler; import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle; @@ -94,7 +102,7 @@ import com.vaadin.shared.util.SharedUtil; |-- AbstractStaticRowContainer | |-- HeaderRowContainer | `-- FooterContainer - `---- BodyRowContainer + `---- BodyRowContainerImpl AbstractRowContainer is intended to contain all common logic between RowContainers. It manages the bookkeeping of row @@ -107,7 +115,7 @@ import com.vaadin.shared.util.SharedUtil; are pretty thin special cases of a StaticRowContainer (mostly relating to positioning of the root element). - BodyRowContainer could also be split into an additional + BodyRowContainerImpl could also be split into an additional "AbstractScrollingRowContainer", but I felt that no more inner classes were needed. So it contains both logic required for making things scroll about, and equivalent @@ -119,8 +127,8 @@ import com.vaadin.shared.util.SharedUtil; Each RowContainer can be thought to have three levels of indices for any given displayed row (but the distinction - matters primarily for the BodyRowContainer, because of the - way it scrolls through data): + matters primarily for the BodyRowContainerImpl, because of + the way it scrolls through data): - Logical index - Physical (or DOM) index @@ -138,9 +146,9 @@ import com.vaadin.shared.util.SharedUtil; (because of 0-based indices). In Header and FooterRowContainers, you are safe to assume that the logical index is the same as the physical index. But because the - BodyRowContainer never displays large data sources entirely - in the DOM, a physical index usually has no apparent direct - relationship with its logical index. + BodyRowContainerImpl never displays large data sources + entirely in the DOM, a physical index usually has no + apparent direct relationship with its logical index. VISUAL INDEX is the index relating to the order that you see a row in, in the browser, as it is rendered. The @@ -148,20 +156,20 @@ import com.vaadin.shared.util.SharedUtil; index is similar to the physical index in the sense that Header and FooterRowContainers can assume a 1:1 relationship between visual index and logical index. And - again, BodyRowContainer has no such relationship. The + again, BodyRowContainerImpl has no such relationship. The body's visual index has additionally no apparent relationship with its physical index. Because the <tr> tags are reused in the body and visually repositioned with CSS as the user scrolls, the relationship between physical index and visual index is quickly broken. You can get an element's visual index via the field - BodyRowContainer.visualRowOrder. + BodyRowContainerImpl.visualRowOrder. Currently, the physical and visual indices are kept in sync _most of the time_ by a deferred rearrangement of rows. They become desynced when scrolling. This is to help screen readers to read the contents from the DOM in a natural - order. See BodyRowContainer.DeferredDomSorter for more + order. See BodyRowContainerImpl.DeferredDomSorter for more about that. */ @@ -264,7 +272,8 @@ abstract class JsniWorkaround { * @since 7.4 * @author Vaadin Ltd */ -public class Escalator extends Widget implements RequiresResize, DeferredWorker { +public class Escalator extends Widget implements RequiresResize, + DeferredWorker, SubPartAware { // todo comments legend /* @@ -273,18 +282,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * re-measure) */ /* - * [[rowheight]]: This code will require alterations that are relevant for - * being able to support variable row heights. NOTE: these bits can most - * often also be identified by searching for code reading the ROW_HEIGHT_PX - * constant. - */ - /* * [[mpixscroll]]: This code will require alterations that are relevant for * supporting the scrolling through more pixels than some browsers normally * would support. (i.e. when we support more than "a million" pixels in the * escalator DOM). NOTE: these bits can most often also be identified by * searching for code that call scrollElem.getScrollTop();. */ + /* + * [[spacer]]: Code that is important to make spacers work. + */ /** * A utility class that contains utility methods that are usually called @@ -812,7 +818,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * that the sizes of the scroll handles appear correct in the browser */ public void recalculateScrollbarsForVirtualViewport() { - double scrollContentHeight = body.calculateTotalRowHeight(); + double scrollContentHeight = body.calculateTotalRowHeight() + + body.spacerContainer.getSpacerHeightsSum(); double scrollContentWidth = columnConfiguration.calculateRowWidth(); double tableWrapperHeight = heightOfEscalator; @@ -820,8 +827,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + WidgetUtil.PIXEL_EPSILON - - header.heightOfSection - - footer.heightOfSection; + - header.getHeightOfSection() + - footer.getHeightOfSection(); boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth + WidgetUtil.PIXEL_EPSILON; @@ -830,8 +837,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (!verticalScrollNeeded && horizontalScrollNeeded) { verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + WidgetUtil.PIXEL_EPSILON - - header.heightOfSection - - footer.heightOfSection + - header.getHeightOfSection() + - footer.getHeightOfSection() - horizontalScrollbar.getScrollbarThickness(); } else { horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth @@ -853,8 +860,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX); tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX); + double footerHeight = footer.getHeightOfSection(); + double headerHeight = header.getHeightOfSection(); double vScrollbarHeight = Math.max(0, tableWrapperHeight - - footer.heightOfSection - header.heightOfSection); + - footerHeight - headerHeight); verticalScrollbar.setOffsetSize(vScrollbarHeight); verticalScrollbar.setScrollSize(scrollContentHeight); @@ -953,6 +962,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker lastScrollTop = scrollTop; body.updateEscalatorRowsOnScroll(); + body.spacerContainer.updateSpacerDecosVisibility(); /* * TODO [[optimize]]: Might avoid a reflow by first calculating new * scrolltop and scrolleft, then doing the escalator magic based on @@ -1142,17 +1152,16 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker public void scrollToRow(final int rowIndex, final ScrollDestination destination, final double padding) { - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - final double targetStartPx = body.getDefaultRowHeight() * rowIndex; + + final double targetStartPx = (body.getDefaultRowHeight() * rowIndex) + + body.spacerContainer + .getSpacerHeightsSumUntilIndex(rowIndex); final double targetEndPx = targetStartPx + body.getDefaultRowHeight(); final double viewportStartPx = getScrollTop(); final double viewportEndPx = viewportStartPx - + body.calculateHeight(); + + body.getHeightOfSection(); final double scrollTop = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); @@ -1177,28 +1186,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ protected final TableSectionElement root; - /** The height of the combined rows in the DOM. Never negative. */ - protected double heightOfSection = 0; - /** * The primary style name of the escalator. Most commonly provided by * Escalator as "v-escalator". */ private String primaryStyleName = null; - /** - * A map containing cached values of an element's current top position. - * <p> - * Don't use this field directly, because it will not take proper care - * of all the bookkeeping required. - * - * @deprecated Use {@link #setRowPosition(Element, int, int)}, - * {@link #getRowTop(Element)} and - * {@link #removeRowPosition(Element)} instead. - */ - @Deprecated - private final Map<TableRowElement, Double> rowTopPositionMap = new HashMap<TableRowElement, Double>(); - private boolean defaultRowHeightShouldBeAutodetected = true; private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; @@ -1710,6 +1703,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); + if (!rowCanBeFrozen(tr)) { + continue; + } TableCellElement cell = tr.getCells().getItem(column); if (frozen) { @@ -1731,12 +1727,29 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); - TableCellElement cell = tr.getCells().getItem(column); - position.set(cell, scrollLeft, 0); + if (rowCanBeFrozen(tr)) { + TableCellElement cell = tr.getCells().getItem(column); + position.set(cell, scrollLeft, 0); + } } } /** + * Checks whether a row is an element, or contains such elements, that + * can be frozen. + * <p> + * In practice, this applies for all header and footer rows. For body + * rows, it applies for all rows except spacer rows. + * + * @param tr + * the row element to check for if it is or has elements that + * can be frozen + * @return <code>true</code> iff this the given element, or any of its + * descendants, can be frozen + */ + abstract protected boolean rowCanBeFrozen(TableRowElement tr); + + /** * Iterates through all the cells in a column and returns the width of * the widest element in this RowContainer. * @@ -1911,20 +1924,29 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ } - @SuppressWarnings("boxing") protected void setRowPosition(final TableRowElement tr, final int x, final double y) { - position.set(tr, x, y); - rowTopPositionMap.put(tr, y); + positions.set(tr, x, y); } - @SuppressWarnings("boxing") + /** + * Returns <em>the assigned</em> top position for the given element. + * <p> + * <em>Note:</em> This method does not calculate what a row's top + * position should be. It just returns an assigned value, correct or + * not. + * + * @param tr + * the table row element to measure + * @return the current top position for {@code tr} + * @see BodyRowContainerImpl#getRowTop(int) + */ protected double getRowTop(final TableRowElement tr) { - return rowTopPositionMap.get(tr); + return positions.getTop(tr); } protected void removeRowPosition(TableRowElement tr) { - rowTopPositionMap.remove(tr); + positions.remove(tr); } public void autodetectRowHeightLater() { @@ -2021,7 +2043,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker TableCellElement cellOriginal = rowElement.getCells().getItem( colIndex); - if (cellIsPartOfSpan(cellOriginal)) { + if (cellOriginal == null || cellIsPartOfSpan(cellOriginal)) { continue; } @@ -2075,10 +2097,24 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker refreshCells(rowRange, colRange); } } + + /** + * The height of this table section. + * <p> + * Note that {@link Escalator#getBody() the body} will calculate its + * height, while the others will return a precomputed value. + * + * @return the height of this table section + */ + protected abstract double getHeightOfSection(); } private abstract class AbstractStaticRowContainer extends AbstractRowContainer { + + /** The height of the combined rows in the DOM. Never negative. */ + private double heightOfSection = 0; + public AbstractStaticRowContainer(final TableSectionElement headElement) { super(headElement); } @@ -2172,9 +2208,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * indices are calculated from the scrollbar position. */ verticalScrollbar.setOffsetSize(heightOfEscalator - - header.heightOfSection - footer.heightOfSection); + - header.getHeightOfSection() + - footer.getHeightOfSection()); body.verifyEscalatorCount(); + body.spacerContainer.updateSpacerDecosVisibility(); } Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight"); @@ -2203,18 +2241,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - /* - * TODO [[rowheight]]: even if no rows are evaluated in the current - * viewport, the heights of some unrendered rows might change in a - * refresh. This would cause the scrollbar to be adjusted (in - * scrollHeight and/or scrollTop). Do we want to take this into - * account? - */ if (hasColumnAndRowData()) { - /* - * TODO [[rowheight]]: nudge rows down with - * refreshRowPositions() as needed - */ for (int row = logicalRowRange.getStart(); row < logicalRowRange .getEnd(); row++) { final TableRowElement tr = getTrByVisualIndex(row); @@ -2229,6 +2256,17 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker protected void paintInsertRows(int visualIndex, int numberOfRows) { paintInsertStaticRows(visualIndex, numberOfRows); } + + @Override + protected boolean rowCanBeFrozen(TableRowElement tr) { + assert root.isOrHasChild(tr) : "Row does not belong to this table section"; + return true; + } + + @Override + protected double getHeightOfSection() { + return Math.max(0, heightOfSection); + } } private class HeaderRowContainer extends AbstractStaticRowContainer { @@ -2238,7 +2276,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker @Override protected void sectionHeightCalculated() { + double heightOfSection = getHeightOfSection(); bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX); + spacerDecoContainer.getStyle().setMarginTop(heightOfSection, + Unit.PX); verticalScrollbar.getElement().getStyle() .setTop(heightOfSection, Unit.PX); headerDeco.getStyle().setHeight(heightOfSection, Unit.PX); @@ -2274,8 +2315,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker @Override protected void sectionHeightCalculated() { + double headerHeight = header.getHeightOfSection(); + double footerHeight = footer.getHeightOfSection(); int vscrollHeight = (int) Math.floor(heightOfEscalator - - header.heightOfSection - footer.heightOfSection); + - headerHeight - footerHeight); final boolean horizontalScrollbarNeeded = columnConfiguration .calculateRowWidth() > widthOfEscalator; @@ -2283,13 +2326,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker vscrollHeight -= horizontalScrollbar.getScrollbarThickness(); } - footerDeco.getStyle().setHeight(footer.heightOfSection, Unit.PX); + footerDeco.getStyle().setHeight(footer.getHeightOfSection(), + Unit.PX); verticalScrollbar.setOffsetSize(vscrollHeight); } } - private class BodyRowContainer extends AbstractRowContainer { + private class BodyRowContainerImpl extends AbstractRowContainer implements + BodyRowContainer { /* * TODO [[optimize]]: check whether a native JsArray might be faster * than LinkedList @@ -2331,7 +2376,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker this.topRowLogicalIndex = topRowLogicalIndex; } - private int getTopRowLogicalIndex() { + public int getTopRowLogicalIndex() { return topRowLogicalIndex; } @@ -2400,7 +2445,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private DeferredDomSorter domSorter = new DeferredDomSorter(); - public BodyRowContainer(final TableSectionElement bodyElement) { + private final SpacerContainer spacerContainer = new SpacerContainer(); + + public BodyRowContainerImpl(final TableSectionElement bodyElement) { super(bodyElement); } @@ -2408,6 +2455,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker public void setStylePrimaryName(String primaryStyleName) { super.setStylePrimaryName(primaryStyleName); UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); + spacerContainer.setStylePrimaryName(primaryStyleName); } public void updateEscalatorRowsOnScroll() { @@ -2417,10 +2465,23 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker boolean rowsWereMoved = false; - final double topRowPos = getRowTop(visualRowOrder.getFirst()); + final double topElementPosition; + final double nextRowBottomOffset; + SpacerContainer.SpacerImpl topSpacer = spacerContainer + .getSpacer(getTopRowLogicalIndex() - 1); + + if (topSpacer != null) { + topElementPosition = topSpacer.getTop(); + nextRowBottomOffset = topSpacer.getHeight() + + getDefaultRowHeight(); + } else { + topElementPosition = getRowTop(visualRowOrder.getFirst()); + nextRowBottomOffset = getDefaultRowHeight(); + } + // TODO [[mpixscroll]] final double scrollTop = tBodyScrollTop; - final double viewportOffset = topRowPos - scrollTop; + final double viewportOffset = topElementPosition - scrollTop; /* * TODO [[optimize]] this if-else can most probably be refactored @@ -2430,22 +2491,17 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (viewportOffset > 0) { // there's empty room on top - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - int originalRowsToMove = (int) Math.ceil(viewportOffset + double rowPx = getRowHeightsSumBetweenPx(scrollTop, + topElementPosition); + int originalRowsToMove = (int) Math.ceil(rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, - root.getChildCount()); + visualRowOrder.size()); - final int end = root.getChildCount(); + final int end = visualRowOrder.size(); final int start = end - rowsToMove; - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + final int logicalRowIndex = getLogicalRowIndex(scrollTop); + moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); @@ -2454,24 +2510,21 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker rowsWereMoved = true; } - else if (viewportOffset + getDefaultRowHeight() <= 0) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - + else if (viewportOffset + nextRowBottomOffset <= 0) { /* * the viewport has been scrolled more than the topmost visual * row. */ - int originalRowsToMove = (int) Math.abs(viewportOffset - / getDefaultRowHeight()); + double rowPx = getRowHeightsSumBetweenPx(topElementPosition, + scrollTop); + + int originalRowsToMove = (int) (rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, - root.getChildCount()); + visualRowOrder.size()); int logicalRowIndex; - if (rowsToMove < root.getChildCount()) { + if (rowsToMove < visualRowOrder.size()) { /* * We scroll so little that we can just keep adding the rows * below the current escalator @@ -2480,15 +2533,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .getLast()) + 1; } else { /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - /* * Since we're moving all escalator rows, we need to * calculate the first logical row index from the scroll * position. */ - logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + logicalRowIndex = getLogicalRowIndex(scrollTop); } /* @@ -2497,13 +2546,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * moveAndUpdateEscalatorRows works, this will work out even if * we move all the rows, and try to place them "at the end". */ - final int targetVisualIndex = root.getChildCount(); + final int targetVisualIndex = visualRowOrder.size(); // make sure that we don't move rows over the data boundary boolean aRowWasLeftBehind = false; if (logicalRowIndex + rowsToMove > getRowCount()) { /* - * TODO [[rowheight]]: with constant row heights, there's + * TODO [[spacer]]: with constant row heights, there's * always exactly one row that will be moved beyond the data * source, when viewport is scrolled to the end. This, * however, isn't guaranteed anymore once row heights start @@ -2513,6 +2562,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker aRowWasLeftBehind = true; } + /* + * Make sure we don't scroll beyond the row content. This can + * happen if we have spacers for the last rows. + */ + rowsToMove = Math.max(0, + Math.min(rowsToMove, getRowCount() - logicalRowIndex)); + moveAndUpdateEscalatorRows(Range.between(0, rowsToMove), targetVisualIndex, logicalRowIndex); @@ -2559,12 +2615,30 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + private double getRowHeightsSumBetweenPx(double y1, double y2) { + assert y1 < y2 : "y1 must be smaller than y2"; + + double viewportPx = y2 - y1; + double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1, + SpacerInclusionStrategy.PARTIAL, y2, + SpacerInclusionStrategy.PARTIAL); + + return viewportPx - spacerPx; + } + + private int getLogicalRowIndex(final double px) { + double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px); + return (int) (rowPx / getDefaultRowHeight()); + } + @Override protected void paintInsertRows(final int index, final int numberOfRows) { if (numberOfRows == 0) { return; } + spacerContainer.shiftSpacersByRows(index, numberOfRows); + /* * TODO: this method should probably only add physical rows, and not * populate them - let everything be populated as appropriate by the @@ -2582,15 +2656,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ scroller.recalculateScrollbarsForVirtualViewport(); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final boolean addedRowsAboveCurrentViewport = index * getDefaultRowHeight() < getScrollTop(); final boolean addedRowsBelowCurrentViewport = index * getDefaultRowHeight() > getScrollTop() - + calculateHeight(); + + getHeightOfSection(); if (addedRowsAboveCurrentViewport) { /* @@ -2599,12 +2669,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * without re-evaluating any rows. */ - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double yDelta = numberOfRows * getDefaultRowHeight(); - adjustScrollPosIgnoreEvents(yDelta); + moveViewportAndContent(yDelta); updateTopRowLogicalIndex(numberOfRows); } @@ -2627,32 +2693,32 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * the remaining rows aswell. */ final int rowsStillNeeded = numberOfRows - addedRows.size(); - final Range unupdatedVisual = convertToVisual(Range.withLength( - unupdatedLogicalStart, rowsStillNeeded)); - final int end = root.getChildCount(); - final int start = end - unupdatedVisual.length(); - final int visualTargetIndex = unupdatedLogicalStart - - visualOffset; - moveAndUpdateEscalatorRows(Range.between(start, end), - visualTargetIndex, unupdatedLogicalStart); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - // move the surrounding rows to their correct places. - double rowTop = (unupdatedLogicalStart + (end - start)) - * getDefaultRowHeight(); - final ListIterator<TableRowElement> i = visualRowOrder - .listIterator(visualTargetIndex + (end - start)); - while (i.hasNext()) { - final TableRowElement tr = i.next(); - setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - rowTop += getDefaultRowHeight(); + if (rowsStillNeeded > 0) { + final Range unupdatedVisual = convertToVisual(Range + .withLength(unupdatedLogicalStart, rowsStillNeeded)); + final int end = getEscalatorRowCount(); + final int start = end - unupdatedVisual.length(); + final int visualTargetIndex = unupdatedLogicalStart + - visualOffset; + moveAndUpdateEscalatorRows(Range.between(start, end), + visualTargetIndex, unupdatedLogicalStart); + + // move the surrounding rows to their correct places. + double rowTop = (unupdatedLogicalStart + (end - start)) + * getDefaultRowHeight(); + final ListIterator<TableRowElement> i = visualRowOrder + .listIterator(visualTargetIndex + (end - start)); + + int logicalRowIndexCursor = unupdatedLogicalStart; + while (i.hasNext()) { + rowTop += spacerContainer + .getSpacerHeight(logicalRowIndexCursor++); + + final TableRowElement tr = i.next(); + setRowPosition(tr, 0, rowTop); + rowTop += getDefaultRowHeight(); + } } fireRowVisibilityChangeEvent(); @@ -2670,12 +2736,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * the visual index where the rows will be placed to * @param logicalTargetIndex * the logical index to be assigned to the first moved row - * @throws IllegalArgumentException - * if any of <code>visualSourceRange.getStart()</code>, - * <code>visualTargetIndex</code> or - * <code>logicalTargetIndex</code> is a negative number; or - * if <code>visualTargetInfo</code> is greater than the - * number of escalator rows. */ private void moveAndUpdateEscalatorRows(final Range visualSourceRange, final int visualTargetIndex, final int logicalTargetIndex) @@ -2685,28 +2745,28 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - if (visualSourceRange.getStart() < 0) { - throw new IllegalArgumentException( - "Logical source start must be 0 or greater (was " - + visualSourceRange.getStart() + ")"); - } else if (logicalTargetIndex < 0) { - throw new IllegalArgumentException( - "Logical target must be 0 or greater"); - } else if (visualTargetIndex < 0) { - throw new IllegalArgumentException( - "Visual target must be 0 or greater"); - } else if (visualTargetIndex > root.getChildCount()) { - throw new IllegalArgumentException( - "Visual target must not be greater than the number of escalator rows"); - } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) { - Range logicalTargetRange = Range.withLength(logicalTargetIndex, - visualSourceRange.length()); - Range availableRange = Range.withLength(0, getRowCount()); - throw new IllegalArgumentException("Logical target leads " - + "to rows outside of the data range (" - + logicalTargetRange + " goes beyond " + availableRange - + ")"); - } + assert visualSourceRange.getStart() >= 0 : "Visual source start " + + "must be 0 or greater (was " + + visualSourceRange.getStart() + ")"; + + assert logicalTargetIndex >= 0 : "Logical target must be 0 or " + + "greater (was " + logicalTargetIndex + ")"; + + assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was " + + visualTargetIndex + ")"; + + assert visualTargetIndex <= getEscalatorRowCount() : "Visual target " + + "must not be greater than the number of escalator rows (was " + + visualTargetIndex + + ", escalator rows " + + getEscalatorRowCount() + ")"; + + assert logicalTargetIndex + visualSourceRange.length() <= getRowCount() : "Logical " + + "target leads to rows outside of the data range (" + + Range.withLength(logicalTargetIndex, + visualSourceRange.length()) + + " goes beyond " + + Range.withLength(0, getRowCount()) + ")"; /* * Since we move a range into another range, the indices might move @@ -2761,55 +2821,74 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } { // Reposition the rows that were moved - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - double newRowTop = logicalTargetIndex * getDefaultRowHeight(); + double newRowTop = getRowTop(logicalTargetIndex); final ListIterator<TableRowElement> iter = visualRowOrder .listIterator(adjustedVisualTargetIndex); for (int i = 0; i < visualSourceRange.length(); i++) { final TableRowElement tr = iter.next(); setRowPosition(tr, 0, newRowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ + newRowTop += getDefaultRowHeight(); + newRowTop += spacerContainer + .getSpacerHeight(logicalTargetIndex + i); } } } /** - * Adjust the scroll position without having the scroll handler have any - * side-effects. + * Adjust the scroll position and move the contained rows. * <p> - * <em>Note:</em> {@link Scroller#onScroll()} <em>will</em> be - * triggered, but it will not do anything, with the help of - * {@link Escalator#internalScrollEventCalls}. + * The difference between using this method and simply scrolling is that + * this method "takes the rows and spacers with it" and renders them + * appropriately. The viewport may be scrolled any arbitrary amount, and + * the contents are moved appropriately, but always snapped into a + * plausible place. + * <p> + * <dl> + * <dt>Example 1</dt> + * <dd>An Escalator with default row height 20px. Adjusting the scroll + * position with 7.5px will move the viewport 7.5px down, but leave the + * row where it is.</dd> + * <dt>Example 2</dt> + * <dd>An Escalator with default row height 20px. Adjusting the scroll + * position with 27.5px will move the viewport 27.5px down, and place + * the row at 20px.</dd> + * </dl> * * @param yDelta - * the delta of pixels to scrolls. A positive value moves the - * viewport downwards, while a negative value moves the - * viewport upwards + * the delta of pixels by which to move the viewport and + * content. A positive value moves everything downwards, + * while a negative value moves everything upwards */ - public void adjustScrollPosIgnoreEvents(final double yDelta) { + public void moveViewportAndContent(final double yDelta) { + if (yDelta == 0) { return; } - verticalScrollbar.setScrollPosByDelta(yDelta); + double newTop = tBodyScrollTop + yDelta; + verticalScrollbar.setScrollPos(newTop); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - final double rowTopPos = yDelta - (yDelta % getDefaultRowHeight()); - for (final TableRowElement tr : visualRowOrder) { - setRowPosition(tr, 0, getRowTop(tr) + rowTopPos); + final double defaultRowHeight = getDefaultRowHeight(); + double rowPxDelta = yDelta - (yDelta % defaultRowHeight); + int rowIndexDelta = (int) (yDelta / defaultRowHeight); + if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) { + + Collection<SpacerContainer.SpacerImpl> spacers = spacerContainer + .getSpacersAfterPx(tBodyScrollTop, + SpacerInclusionStrategy.PARTIAL); + for (SpacerContainer.SpacerImpl spacer : spacers) { + spacer.setPositionDiff(0, rowPxDelta); + spacer.setRowIndex(spacer.getRow() + rowIndexDelta); + } + + for (TableRowElement tr : visualRowOrder) { + setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta); + } } - setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta); + + setBodyScrollPosition(tBodyScrollLeft, newTop); } /** @@ -2831,7 +2910,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final int index, final int numberOfRows) { final int escalatorRowsStillFit = getMaxEscalatorRowCapacity() - - root.getChildCount(); + - getEscalatorRowCount(); final int escalatorRowsNeeded = Math.min(numberOfRows, escalatorRowsStillFit); @@ -2841,43 +2920,31 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker index, escalatorRowsNeeded); visualRowOrder.addAll(index, addedRows); - /* - * We need to figure out the top positions for the rows we just - * added. - */ - for (int i = 0; i < addedRows.size(); i++) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - setRowPosition(addedRows.get(i), 0, (index + i) - * getDefaultRowHeight()); - } + double y = index * getDefaultRowHeight() + + spacerContainer.getSpacerHeightsSumUntilIndex(index); + for (int i = index; i < visualRowOrder.size(); i++) { - /* Move the other rows away from above the added escalator rows */ - for (int i = index + addedRows.size(); i < visualRowOrder - .size(); i++) { - final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - setRowPosition(tr, 0, i * getDefaultRowHeight()); + final TableRowElement tr; + if (i - index < addedRows.size()) { + tr = addedRows.get(i - index); + } else { + tr = visualRowOrder.get(i); + } + + setRowPosition(tr, 0, y); + y += getDefaultRowHeight(); + y += spacerContainer.getSpacerHeight(i); } return addedRows; } else { - return new ArrayList<TableRowElement>(); + return Collections.emptyList(); } } private int getMaxEscalatorRowCapacity() { - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final int maxEscalatorRowCapacity = (int) Math - .ceil(calculateHeight() / getDefaultRowHeight()) + 1; + .ceil(getHeightOfSection() / getDefaultRowHeight()) + 1; /* * maxEscalatorRowCapacity can become negative if the headers and @@ -2897,6 +2964,18 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final Range removedRowsRange = Range .withLength(index, numberOfRows); + /* + * Removing spacers as the very first step will correct the + * scrollbars and row offsets right away. + * + * TODO: actually, it kinda sounds like a Grid feature that a spacer + * would be associated with a particular row. Maybe it would be + * better to have a spacer separate from rows, and simply collapse + * them if they happen to end up on top of each other. This would + * probably make supporting the -1 row pretty easy, too. + */ + spacerContainer.paintRemoveSpacers(removedRowsRange); + final Range[] partitions = removedRowsRange .partitionWith(viewportRange); final Range removedAbove = partitions[0]; @@ -2922,17 +3001,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * absolute 0) * * The logic is optimized in such a way that the - * adjustScrollPosIgnoreEvents is called only once, to avoid extra + * moveViewportAndContent is called only once, to avoid extra * reflows, and thus the code might seem a bit obscure. */ final boolean firstVisualRowIsRemoved = !removedVisualInside .isEmpty() && removedVisualInside.getStart() == 0; if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double yDelta = removedAbove.length() * getDefaultRowHeight(); final double firstLogicalRowHeight = getDefaultRowHeight(); @@ -2946,7 +3021,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * to do is to adjust the scroll position to account for the * removed rows */ - adjustScrollPosIgnoreEvents(-yDelta); + moveViewportAndContent(-yDelta); } else if (removalScrollsToShowFirstLogicalRow) { /* * It seems like we've removed all rows from above, and also @@ -2955,14 +3030,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * current negative scrolltop, presto!), so that it isn't * aligned funnily */ - adjustScrollPosIgnoreEvents(-verticalScrollbar - .getScrollPos()); + moveViewportAndContent(-verticalScrollbar.getScrollPos()); } } // ranges evaluated, let's do things. if (!removedVisualInside.isEmpty()) { - int escalatorRowCount = bodyElem.getChildCount(); + int escalatorRowCount = body.getEscalatorRowCount(); /* * remember: the rows have already been subtracted from the row @@ -2993,13 +3067,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * visualIndex == logicalIndex applies now. */ final int dirtyRowsStart = removedLogicalInside.getStart(); + double y = getRowTop(dirtyRowsStart); for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ - setRowPosition(tr, 0, i * getDefaultRowHeight()); + setRowPosition(tr, 0, y); + y += getDefaultRowHeight(); + y += spacerContainer.getSpacerHeight(i); } /* @@ -3038,14 +3111,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * double-refreshing. */ - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double contentBottom = getRowCount() * getDefaultRowHeight(); final double viewportBottom = tBodyScrollTop - + calculateHeight(); + + getHeightOfSection(); if (viewportBottom <= contentBottom) { /* * We're in the middle of the row container, everything @@ -3096,7 +3165,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ /* - * FIXME [[rowheight]]: above if-clause is coded to only + * FIXME [[spacer]]: above if-clause is coded to only * work with default row heights - will not work with * variable row heights */ @@ -3159,13 +3228,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); setRowPosition(tr, 0, (int) newTop); - - /* - * FIXME [[rowheight]]: coded to work only with - * default row heights - will not work with variable - * row heights - */ newTop += getDefaultRowHeight(); + newTop += spacerContainer.getSpacerHeight(i + + removedLogicalInside.getStart()); } /* @@ -3181,7 +3246,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 5 5 */ final double newScrollTop = contentBottom - - calculateHeight(); + - getHeightOfSection(); setScrollTop(newScrollTop); /* * Manually call the scroll handler, so we get immediate @@ -3213,10 +3278,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 5 */ - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ final int rowsScrolled = (int) (Math .ceil((viewportBottom - contentBottom) / getDefaultRowHeight())); @@ -3268,21 +3329,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final ListIterator<TableRowElement> iterator = visualRowOrder .listIterator(removedVisualInside.getStart()); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - double rowTop = (removedLogicalInside.getStart() + logicalOffset) - * getDefaultRowHeight(); + double rowTop = getRowTop(removedLogicalInside.getStart() + + logicalOffset); for (int i = removedVisualInside.getStart(); i < escalatorRowCount - removedVisualInside.length(); i++) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); + rowTop += spacerContainer.getSpacerHeight(i + + removedLogicalInside.getStart()); } } @@ -3302,22 +3357,18 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker logicalTargetIndex); // move the surrounding rows to their correct places. + int firstUpdatedIndex = removedVisualInside.getEnd(); final ListIterator<TableRowElement> iterator = visualRowOrder - .listIterator(removedVisualInside.getEnd()); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ - double rowTop = removedLogicalInside.getStart() - * getDefaultRowHeight(); + .listIterator(firstUpdatedIndex); + + double rowTop = getRowTop(removedLogicalInside.getStart()); + int i = 0; while (iterator.hasNext()) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); + rowTop += spacerContainer.getSpacerHeight(firstUpdatedIndex + + i++); } } @@ -3355,6 +3406,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * current viewport. The first visual row has the index 0. */ private Range convertToVisual(final Range logicalRange) { + if (logicalRange.isEmpty()) { return logicalRange; } else if (visualRowOrder.isEmpty()) { @@ -3363,8 +3415,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /* - * TODO [[rowheight]]: these assumptions will be totally broken with - * variable row heights. + * TODO [[spacer]]: these assumptions will be totally broken with + * spacers. */ final int maxEscalatorRows = getMaxEscalatorRowCapacity(); final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder @@ -3381,15 +3433,14 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return "td"; } - /** - * Calculates the height of the {@code <tbody>} as it should be rendered - * in the DOM. - */ - private double calculateHeight() { + @Override + protected double getHeightOfSection() { final int tableHeight = tableWrapper.getOffsetHeight(); - final double footerHeight = footer.heightOfSection; - final double headerHeight = header.heightOfSection; - return tableHeight - footerHeight - headerHeight; + final double footerHeight = footer.getHeightOfSection(); + final double headerHeight = header.getHeightOfSection(); + + double heightOfSection = tableHeight - footerHeight - headerHeight; + return Math.max(0, heightOfSection); } @Override @@ -3443,6 +3494,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker tBodyScrollLeft = scrollLeft; tBodyScrollTop = scrollTop; position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); + position.set(spacerDecoContainer, 0, -tBodyScrollTop); } /** @@ -3585,10 +3637,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (!visualRowOrder.isEmpty()) { final double firstRowTop = getRowTop(visualRowOrder .getFirst()); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double firstRowMinTop = tBodyScrollTop - getDefaultRowHeight(); if (firstRowTop < firstRowMinTop) { @@ -3613,20 +3661,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - /* - * As an intermediate step between hard-coded row heights to crazily - * varying row heights, Escalator will support the modification of - * the default row height (which is applied to all rows). - * - * This allows us to do some assumptions and simplifications for - * now. This code is intended to be quite short-lived, but gives - * insight into what needs to be done when row heights change in the - * body, in a general sense. - * - * TODO [[rowheight]] remove this comment once row heights may - * genuinely vary. - */ - Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights"); /* step 1: resize and reposition rows */ @@ -3660,10 +3694,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker /* step 3: make sure we have the correct amount of escalator rows. */ verifyEscalatorCount(); - /* - * TODO [[rowheight]] This simply doesn't work with variable rows - * heights. - */ int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight()); setTopRowLogicalIndex(logicalLogical); @@ -3684,11 +3714,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * its parents are) removed from the document. Therefore, we sort * everything around that row instead. */ - final TableRowElement focusedRow = getEscalatorRowWithFocus(); + final TableRowElement focusedRow = getRowWithFocus(); if (focusedRow != null) { assert focusedRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body"; - assert visualRowOrder.contains(focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder."; + assert visualRowOrder.contains(focusedRow) + || body.spacerContainer.isSpacer(focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer."; } /* @@ -3709,6 +3740,34 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * the first child. */ + List<TableRowElement> orderedBodyRows = new ArrayList<TableRowElement>( + visualRowOrder); + Map<Integer, SpacerContainer.SpacerImpl> spacers = body.spacerContainer + .getSpacers(); + + /* + * Start at -1 to include a spacer that is rendered above the + * viewport, but its parent row is still not shown + */ + for (int i = -1; i < visualRowOrder.size(); i++) { + SpacerContainer.SpacerImpl spacer = spacers.remove(Integer + .valueOf(getTopRowLogicalIndex() + i)); + + if (spacer != null) { + orderedBodyRows.add(i + 1, spacer.getRootElement()); + spacer.show(); + } + } + /* + * At this point, invisible spacers aren't reordered, so their + * position in the DOM will remain undefined. + */ + + // If a spacer was not reordered, it means that it's out of view. + for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) { + unmovedSpacer.hide(); + } + /* * If we have a focused row, start in the mode where we put * everything underneath that row. Otherwise, all rows are placed as @@ -3716,8 +3775,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ boolean insertFirst = (focusedRow == null); - final ListIterator<TableRowElement> i = visualRowOrder - .listIterator(visualRowOrder.size()); + final ListIterator<TableRowElement> i = orderedBodyRows + .listIterator(orderedBodyRows.size()); while (i.hasPrevious()) { TableRowElement tr = i.previous(); @@ -3734,12 +3793,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** - * Get the escalator row that has focus. + * Get the {@literal <tbody>} row that contains (or has) focus. * - * @return The escalator row that contains a focused DOM element, or - * <code>null</code> if focus is outside of a body row. + * @return The {@literal <tbody>} row that contains a focused DOM + * element, or <code>null</code> if focus is outside of a body + * row. */ - private TableRowElement getEscalatorRowWithFocus() { + private TableRowElement getRowWithFocus() { TableRowElement rowContainingFocus = null; final Element focusedElement = WidgetUtil.getFocusedElement(); @@ -3774,6 +3834,99 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), cell.getElement()); } + + @Override + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + spacerContainer.setSpacer(rowIndex, height); + } + + @Override + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + spacerContainer.setSpacerUpdater(spacerUpdater); + } + + @Override + public SpacerUpdater getSpacerUpdater() { + return spacerContainer.getSpacerUpdater(); + } + + /** + * <em>Calculates</em> the correct top position of a row at a logical + * index, regardless if there is one there or not. + * <p> + * A correct result requires that both {@link #getDefaultRowHeight()} is + * consistent, and the placement and height of all spacers above the + * given logical index are consistent. + * + * @param logicalIndex + * the logical index of the row for which to calculate the + * top position + * @return the position at which to place a row in {@code logicalIndex} + * @see #getRowTop(TableRowElement) + */ + private double getRowTop(int logicalIndex) { + double top = spacerContainer + .getSpacerHeightsSumUntilIndex(logicalIndex); + return top + (logicalIndex * getDefaultRowHeight()); + } + + public void shiftRowPositions(int row, double diff) { + for (TableRowElement tr : getVisibleRowsAfter(row)) { + setRowPosition(tr, 0, getRowTop(tr) + diff); + } + } + + private List<TableRowElement> getVisibleRowsAfter(int logicalRow) { + Range visibleRowLogicalRange = getVisibleRowRange(); + + boolean allRowsAreInView = logicalRow < visibleRowLogicalRange + .getStart(); + boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange + .getEnd() - 1; + + if (allRowsAreInView) { + return Collections.unmodifiableList(visualRowOrder); + } else if (noRowsAreInView) { + return Collections.emptyList(); + } else { + int fromIndex = (logicalRow - visibleRowLogicalRange.getStart()) + 1; + int toIndex = visibleRowLogicalRange.length(); + List<TableRowElement> sublist = visualRowOrder.subList( + fromIndex, toIndex); + return Collections.unmodifiableList(sublist); + } + } + + /** + * This method calculates the current escalator row count directly from + * the DOM. + * <p> + * While Escalator is stable, this value should equal to + * {@link #visualRowOrder}.size(), but while row counts are being + * updated, these two values might differ for a short while. + * + * @return the actual DOM count of escalator rows + */ + public int getEscalatorRowCount() { + return root.getChildCount() + - spacerContainer.getSpacersInDom().size(); + } + + @Override + protected boolean rowCanBeFrozen(TableRowElement tr) { + return visualRowOrder.contains(tr); + } + + void reapplySpacerWidths() { + spacerContainer.reapplySpacerWidths(); + } + + void scrollToSpacer(int spacerIndex, ScrollDestination destination, + int padding) { + spacerContainer.scrollToSpacer(spacerIndex, destination, padding); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4396,6 +4549,957 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + /** + * A decision on how to measure a spacer when it is partially within a + * designated range. + * <p> + * The meaning of each value may differ depending on the context it is being + * used in. Check that particular method's JavaDoc. + */ + private enum SpacerInclusionStrategy { + /** A representation of "the entire spacer". */ + COMPLETE, + + /** A representation of "a partial spacer". */ + PARTIAL, + + /** A representation of "no spacer at all". */ + NONE + } + + private class SpacerContainer { + + /** This is used mainly for testing purposes */ + private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; + + private final class SpacerImpl implements Spacer { + private TableCellElement spacerElement; + private TableRowElement root; + private DivElement deco; + private int rowIndex; + private double height = -1; + private boolean domHasBeenSetup = false; + private double decoHeight; + private double defaultCellBorderBottomSize = -1; + + public SpacerImpl(int rowIndex) { + this.rowIndex = rowIndex; + + root = TableRowElement.as(DOM.createTR()); + spacerElement = TableCellElement.as(DOM.createTD()); + root.appendChild(spacerElement); + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + deco = DivElement.as(DOM.createDiv()); + } + + public void setPositionDiff(double x, double y) { + setPosition(getLeft() + x, getTop() + y); + } + + public void setupDom(double height) { + assert !domHasBeenSetup : "DOM can't be set up twice."; + assert RootPanel.get().getElement().isOrHasChild(root) : "Root element should've been attached to the DOM by now."; + domHasBeenSetup = true; + + getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX); + setHeight(height); + + spacerElement.setColSpan(getColumnConfiguration() + .getColumnCount()); + + setStylePrimaryName(getStylePrimaryName()); + } + + public TableRowElement getRootElement() { + return root; + } + + @Override + public Element getDecoElement() { + return deco; + } + + public void setPosition(double x, double y) { + positions.set(getRootElement(), x, y); + positions + .set(getDecoElement(), 0, y - getSpacerDecoTopOffset()); + } + + private double getSpacerDecoTopOffset() { + return getBody().getDefaultRowHeight(); + } + + public void setStylePrimaryName(String style) { + UIObject.setStylePrimaryName(root, style + "-spacer"); + UIObject.setStylePrimaryName(deco, style + "-spacer-deco"); + } + + public void setHeight(double height) { + + assert height >= 0 : "Height must be more >= 0 (was " + height + + ")"; + + final double heightDiff = height - Math.max(0, this.height); + final double oldHeight = this.height; + + this.height = height; + + // since the spacer might be rendered on top of the previous + // rows border (done with css), need to increase height the + // amount of the border thickness + if (defaultCellBorderBottomSize < 0) { + defaultCellBorderBottomSize = WidgetUtil + .getBorderBottomThickness(body.getRowElement( + getVisibleRowRange().getStart()) + .getFirstChildElement()); + } + root.getStyle().setHeight(height + defaultCellBorderBottomSize, + Unit.PX); + + // move the visible spacers getRow row onwards. + shiftSpacerPositionsAfterRow(getRow(), heightDiff); + + /* + * If we're growing, we'll adjust the scroll size first, then + * adjust scrolling. If we're shrinking, we do it after the + * second if-clause. + */ + boolean spacerIsGrowing = heightDiff > 0; + if (spacerIsGrowing) { + verticalScrollbar.setScrollSize(verticalScrollbar + .getScrollSize() + heightDiff); + } + + /* + * Don't modify the scrollbars if we're expanding the -1 spacer + * while we're scrolled to the top. + */ + boolean minusOneSpacerException = spacerIsGrowing + && getRow() == -1 && body.getTopRowLogicalIndex() == 0; + + boolean viewportNeedsScrolling = getRow() < body + .getTopRowLogicalIndex() && !minusOneSpacerException; + if (viewportNeedsScrolling) { + + /* + * We can't use adjustScrollPos here, probably because of a + * bookkeeping-related race condition. + * + * This particular situation is easier, however, since we + * know exactly how many pixels we need to move (heightDiff) + * and all elements below the spacer always need to move + * that pixel amount. + */ + + for (TableRowElement row : body.visualRowOrder) { + body.setRowPosition(row, 0, body.getRowTop(row) + + heightDiff); + } + + double top = getTop(); + double bottom = top + oldHeight; + double scrollTop = verticalScrollbar.getScrollPos(); + + boolean viewportTopIsAtMidSpacer = top < scrollTop + && scrollTop < bottom; + + final double moveDiff; + if (viewportTopIsAtMidSpacer && !spacerIsGrowing) { + + /* + * If the scroll top is in the middle of the modified + * spacer, we want to scroll the viewport up as usual, + * but we don't want to scroll past the top of it. + * + * Math.max ensures this (remember: the result is going + * to be negative). + */ + + moveDiff = Math.max(heightDiff, top - scrollTop); + } else { + moveDiff = heightDiff; + } + body.setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + + moveDiff); + verticalScrollbar.setScrollPosByDelta(moveDiff); + + } else { + body.shiftRowPositions(getRow(), heightDiff); + } + + if (!spacerIsGrowing) { + verticalScrollbar.setScrollSize(verticalScrollbar + .getScrollSize() + heightDiff); + } + + updateDecoratorGeometry(height); + } + + /** Resizes and places the decorator. */ + private void updateDecoratorGeometry(double detailsHeight) { + Style style = deco.getStyle(); + decoHeight = detailsHeight + getBody().getDefaultRowHeight(); + style.setHeight(decoHeight, Unit.PX); + } + + @Override + public Element getElement() { + return spacerElement; + } + + @Override + public int getRow() { + return rowIndex; + } + + public double getHeight() { + assert height >= 0 : "Height was not previously set by setHeight."; + return height; + } + + public double getTop() { + return positions.getTop(getRootElement()); + } + + public double getLeft() { + return positions.getLeft(getRootElement()); + } + + /** + * Sets a new row index for this spacer. Also updates the bookeeping + * at {@link SpacerContainer#rowIndexToSpacer}. + */ + @SuppressWarnings("boxing") + public void setRowIndex(int rowIndex) { + SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex); + assert this == spacer : "trying to move an unexpected spacer."; + this.rowIndex = rowIndex; + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + rowIndexToSpacer.put(this.rowIndex, this); + } + + /** + * Updates the spacer's visibility parameters, based on whether it + * is being currently visible or not. + */ + public void updateVisibility() { + if (isInViewport()) { + show(); + } else { + hide(); + } + } + + private boolean isInViewport() { + int top = (int) Math.ceil(getTop()); + int height = (int) Math.floor(getHeight()); + Range location = Range.withLength(top, height); + return getViewportPixels().intersects(location); + } + + public void show() { + getRootElement().getStyle().clearDisplay(); + getDecoElement().getStyle().clearDisplay(); + } + + public void hide() { + getRootElement().getStyle().setDisplay(Display.NONE); + getDecoElement().getStyle().setDisplay(Display.NONE); + } + + /** + * Crop the decorator element so that it doesn't overlap the header + * and footer sections. + * + * @param bodyTop + * the top cordinate of the escalator body + * @param bodyBottom + * the bottom cordinate of the escalator body + * @param decoWidth + * width of the deco + */ + private void updateDecoClip(final double bodyTop, + final double bodyBottom, final double decoWidth) { + final int top = deco.getAbsoluteTop(); + final int bottom = deco.getAbsoluteBottom(); + if (top < bodyTop || bottom > bodyBottom) { + final double topClip = Math.max(0.0D, bodyTop - top); + final double bottomClip = decoHeight + - Math.max(0.0D, bottom - bodyBottom); + // TODO [optimize] not sure how GWT compiles this + final String clip = new StringBuilder("rect(") + .append(topClip).append("px,").append(decoWidth) + .append("px,").append(bottomClip).append("px,0)") + .toString(); + deco.getStyle().setProperty("clip", clip); + } else { + deco.getStyle().setProperty("clip", "auto"); + } + } + } + + private final TreeMap<Integer, SpacerImpl> rowIndexToSpacer = new TreeMap<Integer, SpacerImpl>(); + + private SpacerUpdater spacerUpdater = SpacerUpdater.NULL; + + private final ScrollHandler spacerScroller = new ScrollHandler() { + private double prevScrollX = 0; + + @Override + public void onScroll(ScrollEvent event) { + if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) { + return; + } + + prevScrollX = getScrollLeft(); + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.setPosition(prevScrollX, spacer.getTop()); + } + } + }; + private HandlerRegistration spacerScrollerRegistration; + + /** Width of the spacers' decos. Calculated once then cached. */ + private double spacerDecoWidth = 0.0D; + + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + + if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) { + throw new IllegalArgumentException("invalid row index: " + + rowIndex + ", while the body only has " + + getBody().getRowCount() + " rows."); + } + + if (height >= 0) { + if (!spacerExists(rowIndex)) { + insertNewSpacer(rowIndex, height); + } else { + updateExistingSpacer(rowIndex, height); + } + } else if (spacerExists(rowIndex)) { + removeSpacer(rowIndex); + } + } + + /** Checks if a given element is a spacer element */ + public boolean isSpacer(TableRowElement focusedRow) { + + /* + * If this needs optimization, we could do a more heuristic check + * based on stylenames and stuff, instead of iterating through the + * map. + */ + + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + if (spacer.getRootElement().equals(focusedRow)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("boxing") + void scrollToSpacer(int spacerIndex, ScrollDestination destination, + int padding) { + + assert !destination.equals(ScrollDestination.MIDDLE) + || padding != 0 : "destination/padding check should be done before this method"; + + if (!rowIndexToSpacer.containsKey(spacerIndex)) { + throw new IllegalArgumentException("No spacer open at index " + + spacerIndex); + } + + SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex); + double targetStartPx = spacer.getTop(); + double targetEndPx = targetStartPx + spacer.getHeight(); + + Range viewportPixels = getViewportPixels(); + double viewportStartPx = viewportPixels.getStart(); + double viewportEndPx = viewportPixels.getEnd(); + + double scrollTop = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + setScrollTop(scrollTop); + } + + public void reapplySpacerWidths() { + // FIXME #16266 , spacers get couple pixels too much because borders + final double width = getInnerWidth() - spacerDecoWidth; + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.getRootElement().getStyle().setWidth(width, Unit.PX); + } + } + + public void paintRemoveSpacers(Range removedRowsRange) { + removeSpacers(removedRowsRange); + shiftSpacersByRows(removedRowsRange.getStart(), + -removedRowsRange.length()); + } + + @SuppressWarnings("boxing") + public void removeSpacers(Range removedRange) { + + Map<Integer, SpacerImpl> removedSpacers = rowIndexToSpacer + .subMap(removedRange.getStart(), true, + removedRange.getEnd(), false); + + if (removedSpacers.isEmpty()) { + return; + } + + for (SpacerImpl spacer : removedSpacers.values()) { + /* + * [[optimization]] TODO: Each invocation of the setHeight + * method has a cascading effect in the DOM. if this proves to + * be slow, the DOM offset could be updated as a batch. + */ + + destroySpacerContent(spacer); + spacer.setHeight(0); // resets row offsets + spacer.getRootElement().removeFromParent(); + spacer.getDecoElement().removeFromParent(); + } + + removedSpacers.clear(); + + if (rowIndexToSpacer.isEmpty()) { + assert spacerScrollerRegistration != null : "Spacer scroller registration was null"; + spacerScrollerRegistration.removeHandler(); + spacerScrollerRegistration = null; + } + } + + public Map<Integer, SpacerImpl> getSpacers() { + return new HashMap<Integer, SpacerImpl>(rowIndexToSpacer); + } + + /** + * Calculates the sum of all spacers. + * + * @return sum of all spacers, or 0 if no spacers present + */ + public double getSpacerHeightsSum() { + return getHeights(rowIndexToSpacer.values()); + } + + /** + * Calculates the sum of all spacers from one row index onwards. + * + * @param logicalRowIndex + * the spacer to include as the first calculated spacer + * @return the sum of all spacers from {@code logicalRowIndex} and + * onwards, or 0 if no suitable spacers were found + */ + @SuppressWarnings("boxing") + public Collection<SpacerImpl> getSpacersForRowAndAfter( + int logicalRowIndex) { + return new ArrayList<SpacerImpl>(rowIndexToSpacer.tailMap( + logicalRowIndex, true).values()); + } + + /** + * Get all spacers from one pixel point onwards. + * <p> + * + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + * <dl> + * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + * <dd>include the spacer + * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + * <dd>include the spacer + * <dt>{@link SpacerInclusionStrategy#NONE NONE} + * <dd>ignore the spacer + * </dl> + * + * @param px + * the pixel point after which to return all spacers + * @param strategy + * the inclusion strategy regarding the {@code px} + * @return a collection of the spacers that exist after {@code px} + */ + public Collection<SpacerImpl> getSpacersAfterPx(final double px, + final SpacerInclusionStrategy strategy) { + + ArrayList<SpacerImpl> spacers = new ArrayList<SpacerImpl>( + rowIndexToSpacer.values()); + + for (int i = 0; i < spacers.size(); i++) { + SpacerImpl spacer = spacers.get(i); + + double top = spacer.getTop(); + double bottom = top + spacer.getHeight(); + + if (top > px) { + return spacers.subList(i, spacers.size()); + } else if (bottom > px) { + if (strategy == SpacerInclusionStrategy.NONE) { + return spacers.subList(i + 1, spacers.size()); + } else { + return spacers.subList(i, spacers.size()); + } + } + } + + return Collections.emptySet(); + } + + /** + * Gets the spacers currently rendered in the DOM. + * + * @return an unmodifiable (but live) collection of the spacers + * currently in the DOM + */ + public Collection<SpacerImpl> getSpacersInDom() { + return Collections + .unmodifiableCollection(rowIndexToSpacer.values()); + } + + /** + * Gets the amount of pixels occupied by spacers between two pixel + * points. + * <p> + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + * <dl> + * <dt>{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + * <dd>take the entire spacer into account + * <dt>{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + * <dd>take only the visible area into account + * <dt>{@link SpacerInclusionStrategy#NONE NONE} + * <dd>ignore that spacer + * </dl> + * + * @param rangeTop + * the top pixel point + * @param topInclusion + * the inclusion strategy regarding {@code rangeTop}. + * @param rangeBottom + * the bottom pixel point + * @param bottomInclusion + * the inclusion strategy regarding {@code rangeBottom}. + * @return the pixels occupied by spacers between {@code rangeTop} and + * {@code rangeBottom} + */ + public double getSpacerHeightsSumBetweenPx(double rangeTop, + SpacerInclusionStrategy topInclusion, double rangeBottom, + SpacerInclusionStrategy bottomInclusion) { + + assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom"; + + double heights = 0; + + /* + * TODO [[optimize]]: this might be somewhat inefficient (due to + * iterator-based scanning, instead of using the treemap's search + * functionalities). But it should be easy to write, read, verify + * and maintain. + */ + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + double top = spacer.getTop(); + double height = spacer.getHeight(); + double bottom = top + height; + + /* + * If we happen to implement a DoubleRange (in addition to the + * int-based Range) at some point, the following logic should + * probably be converted into using the + * Range.partitionWith-equivalent. + */ + + boolean topIsAboveRange = top < rangeTop; + boolean topIsInRange = rangeTop <= top && top <= rangeBottom; + boolean topIsBelowRange = rangeBottom < top; + + boolean bottomIsAboveRange = bottom < rangeTop; + boolean bottomIsInRange = rangeTop <= bottom + && bottom <= rangeBottom; + boolean bottomIsBelowRange = rangeBottom < bottom; + + assert topIsAboveRange ^ topIsBelowRange ^ topIsInRange : "Bad top logic"; + assert bottomIsAboveRange ^ bottomIsBelowRange + ^ bottomIsInRange : "Bad bottom logic"; + + if (bottomIsAboveRange) { + continue; + } else if (topIsBelowRange) { + return heights; + } + + else if (topIsAboveRange && bottomIsInRange) { + switch (topInclusion) { + case PARTIAL: + heights += bottom - rangeTop; + break; + case COMPLETE: + heights += height; + break; + default: + break; + } + } + + else if (topIsAboveRange && bottomIsBelowRange) { + + /* + * Here we arbitrarily decide that the top inclusion will + * have the honor of overriding the bottom inclusion if + * happens to be a conflict of interests. + */ + switch (topInclusion) { + case NONE: + return 0; + case COMPLETE: + return height; + case PARTIAL: + return rangeBottom - rangeTop; + default: + throw new IllegalArgumentException( + "Unexpected inclusion state :" + topInclusion); + } + + } else if (topIsInRange && bottomIsInRange) { + heights += height; + } + + else if (topIsInRange && bottomIsBelowRange) { + switch (bottomInclusion) { + case PARTIAL: + heights += rangeBottom - top; + break; + case COMPLETE: + heights += height; + break; + default: + break; + } + + return heights; + } + + else { + assert false : "Unnaccounted-for situation"; + } + } + + return heights; + } + + /** + * Gets the amount of pixels occupied by spacers from the top until a + * certain spot from the top of the body. + * + * @param px + * pixels counted from the top + * @return the pixels occupied by spacers up until {@code px} + */ + public double getSpacerHeightsSumUntilPx(double px) { + return getSpacerHeightsSumBetweenPx(0, + SpacerInclusionStrategy.PARTIAL, px, + SpacerInclusionStrategy.PARTIAL); + } + + /** + * Gets the amount of pixels occupied by spacers until a logical row + * index. + * + * @param logicalIndex + * a logical row index + * @return the pixels occupied by spacers up until {@code logicalIndex} + */ + @SuppressWarnings("boxing") + public double getSpacerHeightsSumUntilIndex(int logicalIndex) { + return getHeights(rowIndexToSpacer.headMap(logicalIndex, false) + .values()); + } + + private double getHeights(Collection<SpacerImpl> spacers) { + double heights = 0; + for (SpacerImpl spacer : spacers) { + heights += spacer.getHeight(); + } + return heights; + } + + /** + * Gets the height of the spacer for a row index. + * + * @param rowIndex + * the index of the row where the spacer should be + * @return the height of the spacer at index {@code rowIndex}, or 0 if + * there is no spacer there + */ + public double getSpacerHeight(int rowIndex) { + SpacerImpl spacer = getSpacer(rowIndex); + if (spacer != null) { + return spacer.getHeight(); + } else { + return 0; + } + } + + private boolean spacerExists(int rowIndex) { + return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex)); + } + + @SuppressWarnings("boxing") + private void insertNewSpacer(int rowIndex, double height) { + + if (spacerScrollerRegistration == null) { + spacerScrollerRegistration = addScrollHandler(spacerScroller); + } + + final SpacerImpl spacer = new SpacerImpl(rowIndex); + + rowIndexToSpacer.put(rowIndex, spacer); + // set the position before adding it to DOM + positions.set(spacer.getRootElement(), getScrollLeft(), + calculateSpacerTop(rowIndex)); + + TableRowElement spacerRoot = spacer.getRootElement(); + spacerRoot.getStyle().setWidth( + columnConfiguration.calculateRowWidth(), Unit.PX); + body.getElement().appendChild(spacerRoot); + spacer.setupDom(height); + // set the deco position, requires that spacer is in the DOM + positions.set(spacer.getDecoElement(), 0, + spacer.getTop() - spacer.getSpacerDecoTopOffset()); + + spacerDecoContainer.appendChild(spacer.getDecoElement()); + if (spacerDecoContainer.getParentElement() == null) { + getElement().appendChild(spacerDecoContainer); + // calculate the spacer deco width, it won't change + spacerDecoWidth = WidgetUtil + .getRequiredWidthBoundingClientRectDouble(spacer + .getDecoElement()); + } + + initSpacerContent(spacer); + + body.sortDomElements(); + } + + private void updateExistingSpacer(int rowIndex, double newHeight) { + getSpacer(rowIndex).setHeight(newHeight); + } + + public SpacerImpl getSpacer(int rowIndex) { + return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); + } + + private void removeSpacer(int rowIndex) { + removeSpacers(Range.withOnly(rowIndex)); + } + + public void setStylePrimaryName(String style) { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.setStylePrimaryName(style); + } + } + + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + if (spacerUpdater == null) { + throw new IllegalArgumentException( + "spacer updater cannot be null"); + } + + destroySpacerContent(rowIndexToSpacer.values()); + this.spacerUpdater = spacerUpdater; + initSpacerContent(rowIndexToSpacer.values()); + } + + public SpacerUpdater getSpacerUpdater() { + return spacerUpdater; + } + + private void destroySpacerContent(Iterable<SpacerImpl> spacers) { + for (SpacerImpl spacer : spacers) { + destroySpacerContent(spacer); + } + } + + private void destroySpacerContent(SpacerImpl spacer) { + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before detaching"; + spacerUpdater.destroy(spacer); + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before detaching"; + } + + private void initSpacerContent(Iterable<SpacerImpl> spacers) { + for (SpacerImpl spacer : spacers) { + initSpacerContent(spacer); + } + } + + private void initSpacerContent(SpacerImpl spacer) { + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator before attaching"; + spacerUpdater.init(spacer); + assert getElement().isOrHasChild(spacer.getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching"; + assert getElement().isOrHasChild(spacer.getElement()) : "Spacer element somehow got detached from Escalator during attaching"; + + spacer.updateVisibility(); + } + + public String getSubPartName(Element subElement) { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + if (spacer.getRootElement().isOrHasChild(subElement)) { + return "spacer[" + spacer.getRow() + "]"; + } + } + return null; + } + + public Element getSubPartElement(int index) { + SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index)); + if (spacer != null) { + return spacer.getElement(); + } else { + return null; + } + } + + private double calculateSpacerTop(int logicalIndex) { + return body.getRowTop(logicalIndex) + body.getDefaultRowHeight(); + } + + @SuppressWarnings("boxing") + private void shiftSpacerPositionsAfterRow(int changedRowIndex, + double diffPx) { + for (SpacerImpl spacer : rowIndexToSpacer.tailMap(changedRowIndex, + false).values()) { + spacer.setPositionDiff(0, diffPx); + } + } + + /** + * Shifts spacers at and after a specific row by an amount of rows. + * <p> + * This moves both their associated row index and also their visual + * placement. + * <p> + * <em>Note:</em> This method does not check for the validity of any + * arguments. + * + * @param index + * the index of first row to move + * @param numberOfRows + * the number of rows to shift the spacers with. A positive + * value is downwards, a negative value is upwards. + */ + public void shiftSpacersByRows(int index, int numberOfRows) { + final double pxDiff = numberOfRows * body.getDefaultRowHeight(); + for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter(index)) { + spacer.setPositionDiff(0, pxDiff); + spacer.setRowIndex(spacer.getRow() + numberOfRows); + } + } + + private void updateSpacerDecosVisibility() { + final Range visibleRowRange = getVisibleRowRange(); + Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer.subMap( + visibleRowRange.getStart() - 1, + visibleRowRange.getEnd() + 1).values(); + if (!visibleSpacers.isEmpty()) { + final double top = tableWrapper.getAbsoluteTop() + + header.getHeightOfSection(); + final double bottom = tableWrapper.getAbsoluteBottom() + - footer.getHeightOfSection(); + for (SpacerImpl spacer : visibleSpacers) { + spacer.updateDecoClip(top, bottom, spacerDecoWidth); + } + } + } + } + + private class ElementPositionBookkeeper { + /** + * A map containing cached values of an element's current top position. + */ + private final Map<Element, Double> elementTopPositionMap = new HashMap<Element, Double>(); + private final Map<Element, Double> elementLeftPositionMap = new HashMap<Element, Double>(); + + public void set(final Element e, final double x, final double y) { + assert e != null : "Element was null"; + position.set(e, x, y); + elementTopPositionMap.put(e, Double.valueOf(y)); + elementLeftPositionMap.put(e, Double.valueOf(x)); + } + + public double getTop(final Element e) { + Double top = elementTopPositionMap.get(e); + if (top == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return top.doubleValue(); + } + + public double getLeft(final Element e) { + Double left = elementLeftPositionMap.get(e); + if (left == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return left.doubleValue(); + } + + public void remove(Element e) { + elementTopPositionMap.remove(e); + elementLeftPositionMap.remove(e); + } + } + + public static class SubPartArguments { + private String type; + private int[] indices; + + private SubPartArguments(String type, int[] indices) { + /* + * The constructor is private so that no third party would by + * mistake start using this parsing scheme, since it's not official + * by TestBench (yet?). + */ + + this.type = type; + this.indices = indices; + } + + public String getType() { + return type; + } + + public int getIndicesLength() { + return indices.length; + } + + public int getIndex(int i) { + return indices[i]; + } + + public int[] getIndices() { + return Arrays.copyOf(indices, indices.length); + } + + static SubPartArguments create(String subPart) { + String[] splitArgs = subPart.split("\\["); + String type = splitArgs[0]; + int[] indices = new int[splitArgs.length - 1]; + for (int i = 0; i < indices.length; ++i) { + String tmp = splitArgs[i + 1]; + indices[i] = Integer + .parseInt(tmp.substring(0, tmp.length() - 1)); + } + return new SubPartArguments(type, indices); + } + } + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y /** * The solution to @@ -4455,7 +5559,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); private final HeaderRowContainer header = new HeaderRowContainer(headElem); - private final BodyRowContainer body = new BodyRowContainer(bodyElem); + private final BodyRowContainerImpl body = new BodyRowContainerImpl(bodyElem); private final FooterRowContainer footer = new FooterRowContainer(footElem); private final Scroller scroller = new Scroller(); @@ -4467,6 +5571,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .createDiv()); private final DivElement headerDeco = DivElement.as(DOM.createDiv()); private final DivElement footerDeco = DivElement.as(DOM.createDiv()); + private final DivElement spacerDecoContainer = DivElement.as(DOM + .createDiv()); private PositionFunction position; @@ -4494,6 +5600,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private final SubpixelBrowserBugDetector subpixelBrowserBugDetector = new SubpixelBrowserBugDetector(); + private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper(); + /** * Creates a new Escalator widget instance. */ @@ -4540,6 +5648,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker setStylePrimaryName("v-escalator"); + spacerDecoContainer.setAttribute("aria-hidden", "true"); + // init default dimensions setHeight(null); setWidth(null); @@ -4704,12 +5814,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * updated correctly. Since it isn't, we'll simply and brutally rip out * the DOM elements (in an elegant way, of course). */ - int rowsToRemove = bodyElem.getChildCount(); + int rowsToRemove = body.getEscalatorRowCount(); for (int i = 0; i < rowsToRemove; i++) { int index = rowsToRemove - i - 1; TableRowElement tr = bodyElem.getRows().getItem(index); body.paintRemoveRow(tr, index); - body.removeRowPosition(tr); + positions.remove(tr); } body.visualRowOrder.clear(); body.setTopRowLogicalIndex(0); @@ -4787,7 +5897,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * * @return the body. Never <code>null</code> */ - public RowContainer getBody() { + public BodyRowContainer getBody() { return body; } @@ -4906,6 +6016,28 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** + * Returns the scroll width for the escalator. Note that this is not + * necessary the same as {@code Element.scrollWidth} in the DOM. + * + * @since 7.5.0 + * @return the scroll width in pixels + */ + public double getScrollWidth() { + return horizontalScrollbar.getScrollSize(); + } + + /** + * Returns the scroll height for the escalator. Note that this is not + * necessary the same as {@code Element.scrollHeight} in the DOM. + * + * @since 7.5.0 + * @return the scroll height in pixels + */ + public double getScrollHeight() { + return verticalScrollbar.getScrollSize(); + } + + /** * Scrolls the body horizontally so that the column at the given index is * visible and there is at least {@code padding} pixels in the direction of * the given scroll destination. @@ -4922,15 +6054,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * column * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} - * and padding is nonzero, or if the indicated column is frozen + * and padding is nonzero; or if the indicated column is frozen; + * or if {@code destination == null} */ public void scrollToColumn(final int columnIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - if (destination == ScrollDestination.MIDDLE && padding != 0) { - throw new IllegalArgumentException( - "You cannot have a padding with a MIDDLE destination"); - } + validateScrollDestination(destination, padding); verifyValidColumnIndex(columnIndex); if (columnIndex < columnConfiguration.frozenColumns) { @@ -4966,15 +6096,14 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * if {@code rowIndex} is not a valid index for an existing row * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} - * and padding is nonzero + * and padding is nonzero; or if {@code destination == null} + * @see #scrollToRowAndSpacer(int, ScrollDestination, int) + * @see #scrollToSpacer(int, ScrollDestination, int) */ public void scrollToRow(final int rowIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - if (destination == ScrollDestination.MIDDLE && padding != 0) { - throw new IllegalArgumentException( - "You cannot have a padding with a MIDDLE destination"); - } + validateScrollDestination(destination, padding); verifyValidRowIndex(rowIndex); scroller.scrollToRow(rowIndex, destination, padding); @@ -4988,6 +6117,120 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** + * Scrolls the body vertically so that the spacer at the given row index is + * visible and there is at least {@literal padding} pixesl to the given + * scroll destination. + * + * @since + * @param spacerIndex + * the row index of the spacer to scroll to + * @param destination + * where the spacer should be aligned visually after scrolling + * @param padding + * the number of pixels to place between the scrolled-to spacer + * and the viewport edge + * @throws IllegalArgumentException + * if {@code spacerIndex} is not an opened spacer; or if + * {@code destination} is {@link ScrollDestination#MIDDLE} and + * padding is nonzero; or if {@code destination == null} + * @see #scrollToRow(int, ScrollDestination, int) + * @see #scrollToRowAndSpacer(int, ScrollDestination, int) + */ + public void scrollToSpacer(final int spacerIndex, + ScrollDestination destination, final int padding) + throws IllegalArgumentException { + validateScrollDestination(destination, padding); + body.scrollToSpacer(spacerIndex, destination, padding); + } + + /** + * Scrolls vertically to a row and the spacer below it. + * <p> + * If a spacer is not open at that index, this method behaves like + * {@link #scrollToRow(int, ScrollDestination, int)} + * + * @since + * @param rowIndex + * the index of the logical row to scroll to. -1 takes the + * topmost spacer into account as well. + * @param destination + * where the row should be aligned visually after scrolling + * @param padding + * the number pixels to place between the scrolled-to row and the + * viewport edge. + * @see #scrollToRow(int, ScrollDestination, int) + * @see #scrollToSpacer(int, ScrollDestination, int) + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and {@code padding} is not zero; or if {@code rowIndex} is + * not a valid row index, or -1; or if + * {@code destination == null}; or if {@code rowIndex == -1} and + * there is no spacer open at that index. + */ + public void scrollToRowAndSpacer(int rowIndex, + ScrollDestination destination, int padding) + throws IllegalArgumentException { + validateScrollDestination(destination, padding); + if (rowIndex != -1) { + verifyValidRowIndex(rowIndex); + } + + // row range + final Range rowRange; + if (rowIndex != -1) { + int rowTop = (int) Math.floor(body.getRowTop(rowIndex)); + int rowHeight = (int) Math.ceil(body.getDefaultRowHeight()); + rowRange = Range.withLength(rowTop, rowHeight); + } else { + rowRange = Range.withLength(0, 0); + } + + // get spacer + final SpacerContainer.SpacerImpl spacer = body.spacerContainer + .getSpacer(rowIndex); + + if (rowIndex == -1 && spacer == null) { + throw new IllegalArgumentException("Cannot scroll to row index " + + "-1, as there is no spacer open at that index."); + } + + // make into target range + final Range targetRange; + if (spacer != null) { + final int spacerTop = (int) Math.floor(spacer.getTop()); + final int spacerHeight = (int) Math.ceil(spacer.getHeight()); + Range spacerRange = Range.withLength(spacerTop, spacerHeight); + + targetRange = rowRange.combineWith(spacerRange); + } else { + targetRange = rowRange; + } + + // get params + int targetStart = targetRange.getStart(); + int targetEnd = targetRange.getEnd(); + double viewportStart = getScrollTop(); + double viewportEnd = viewportStart + body.getHeightOfSection(); + + double scrollPos = getScrollPos(destination, targetStart, targetEnd, + viewportStart, viewportEnd, padding); + + setScrollTop(scrollPos); + } + + private static void validateScrollDestination( + final ScrollDestination destination, final int padding) { + if (destination == null) { + throw new IllegalArgumentException("Destination cannot be null"); + } + + if (destination == ScrollDestination.MIDDLE && padding != 0) { + throw new IllegalArgumentException( + "You cannot have a padding with a MIDDLE destination"); + } + } + + /** * Recalculates the dimensions for all elements that require manual * calculations. Also updates the dimension caches. * <p> @@ -5014,6 +6257,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker scroller.recalculateScrollbarsForVirtualViewport(); body.verifyEscalatorCount(); + body.reapplySpacerWidths(); Profiler.leave("Escalator.recalculateElementSizes"); } @@ -5051,7 +6295,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker /** * Adds an event handler that gets notified when the range of visible rows - * changes e.g. because of scrolling or row resizing. + * changes e.g. because of scrolling, row resizing or spacers + * appearing/disappearing. * * @param rowVisibilityChangeHandler * the event handler @@ -5079,14 +6324,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /** - * Gets the range of currently visible rows. + * Gets the logical index range of currently visible rows. * - * @return range of visible rows + * @return logical index range of visible rows */ public Range getVisibleRowRange() { if (!body.visualRowOrder.isEmpty()) { - return Range.withLength( - body.getLogicalRowIndex(body.visualRowOrder.getFirst()), + return Range.withLength(body.getTopRowLogicalIndex(), body.visualRowOrder.size()); } else { return Range.withLength(0, 0); @@ -5130,6 +6374,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco"); UIObject.setStylePrimaryName(horizontalScrollbarDeco, style + "-horizontal-scrollbar-deco"); + UIObject.setStylePrimaryName(spacerDecoContainer, style + + "-spacer-deco-container"); header.setStylePrimaryName(style); body.setStylePrimaryName(style); @@ -5188,8 +6434,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - double headerHeight = header.heightOfSection; - double footerHeight = footer.heightOfSection; + double headerHeight = header.getHeightOfSection(); + double footerHeight = footer.getHeightOfSection(); double bodyHeight = body.getDefaultRowHeight() * heightByRows; double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar .getScrollbarThickness() : 0; @@ -5382,4 +6628,181 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker columnConfiguration.getColumnWidth(i)); } } + + private Range getViewportPixels() { + int from = (int) Math.floor(verticalScrollbar.getScrollPos()); + int to = (int) body.getHeightOfSection(); + return Range.withLength(from, to); + } + + @Override + @SuppressWarnings("deprecation") + public com.google.gwt.user.client.Element getSubPartElement(String subPart) { + SubPartArguments args = parseSubPartArguments(subPart); + + Element tableStructureElement = getSubPartElementTableStructure(args); + if (tableStructureElement != null) { + return DOM.asOld(tableStructureElement); + } + + Element spacerElement = getSubPartElementSpacer(args); + if (spacerElement != null) { + return DOM.asOld(spacerElement); + } + + return null; + } + + private Element getSubPartElementTableStructure(SubPartArguments args) { + + String type = args.getType(); + int[] indices = args.getIndices(); + + // Get correct RowContainer for type from Escalator + RowContainer container = null; + if (type.equalsIgnoreCase("header")) { + container = getHeader(); + } else if (type.equalsIgnoreCase("cell")) { + // If wanted row is not visible, we need to scroll there. + Range visibleRowRange = getVisibleRowRange(); + if (indices.length > 0 && !visibleRowRange.contains(indices[0])) { + try { + scrollToRow(indices[0], ScrollDestination.ANY, 0); + } catch (IllegalArgumentException e) { + getLogger().log(Level.SEVERE, e.getMessage()); + } + // Scrolling causes a lazy loading event. No element can + // currently be retrieved. + return null; + } + container = getBody(); + } else if (type.equalsIgnoreCase("footer")) { + container = getFooter(); + } + + if (null != container) { + if (indices.length == 0) { + // No indexing. Just return the wanted container element + return container.getElement(); + } else { + try { + return getSubPart(container, indices); + } catch (Exception e) { + getLogger().log(Level.SEVERE, e.getMessage()); + } + } + } + return null; + } + + private Element getSubPart(RowContainer container, int[] indices) { + Element targetElement = container.getRowElement(indices[0]); + + // Scroll wanted column to view if able + if (indices.length > 1 && targetElement != null) { + if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) { + scrollToColumn(indices[1], ScrollDestination.ANY, 0); + } + + targetElement = getCellFromRow(TableRowElement.as(targetElement), + indices[1]); + + for (int i = 2; i < indices.length && targetElement != null; ++i) { + targetElement = (Element) targetElement.getChild(indices[i]); + } + } + + return targetElement; + } + + private static Element getCellFromRow(TableRowElement rowElement, int index) { + int childCount = rowElement.getCells().getLength(); + if (index < 0 || index >= childCount) { + return null; + } + + TableCellElement currentCell = null; + boolean indexInColspan = false; + int i = 0; + + while (!indexInColspan) { + currentCell = rowElement.getCells().getItem(i); + + // Calculate if this is the cell we are looking for + int colSpan = currentCell.getColSpan(); + indexInColspan = index < colSpan + i; + + // Increment by colspan to skip over hidden cells + i += colSpan; + } + return currentCell; + } + + private Element getSubPartElementSpacer(SubPartArguments args) { + if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) { + return body.spacerContainer.getSubPartElement(args.getIndex(0)); + } else { + return null; + } + } + + @Override + @SuppressWarnings("deprecation") + public String getSubPartName(com.google.gwt.user.client.Element subElement) { + + /* + * The spacer check needs to be before table structure check, because + * (for now) the table structure will take spacer elements into account + * as well, when it shouldn't. + */ + + String spacer = getSubPartNameSpacer(subElement); + if (spacer != null) { + return spacer; + } + + String tableStructure = getSubPartNameTableStructure(subElement); + if (tableStructure != null) { + return tableStructure; + } + + return null; + } + + private String getSubPartNameTableStructure(Element subElement) { + + List<RowContainer> containers = Arrays.asList(getHeader(), getBody(), + getFooter()); + List<String> containerType = Arrays.asList("header", "cell", "footer"); + + for (int i = 0; i < containers.size(); ++i) { + RowContainer container = containers.get(i); + boolean containerRow = (subElement.getTagName().equalsIgnoreCase( + "tr") && subElement.getParentElement() == container + .getElement()); + if (containerRow) { + /* + * Wanted SubPart is row that is a child of containers root to + * get indices, we use a cell that is a child of this row + */ + subElement = subElement.getFirstChildElement(); + } + + Cell cell = container.getCell(subElement); + if (cell != null) { + // Skip the column index if subElement was a child of root + return containerType.get(i) + "[" + cell.getRow() + + (containerRow ? "]" : "][" + cell.getColumn() + "]"); + } + } + return null; + } + + private String getSubPartNameSpacer(Element subElement) { + return body.spacerContainer.getSubPartName(subElement); + } + + public static SubPartArguments parseSubPartArguments(String subPart) { + return SubPartArguments.create(subPart); + } } diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 84ba3e5704..4951997995 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -25,7 +25,9 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,6 +38,8 @@ import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.EventTarget; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; @@ -45,6 +49,8 @@ import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyEvent; import com.google.gwt.event.dom.client.MouseEvent; import com.google.gwt.event.logical.shared.ValueChangeEvent; @@ -53,11 +59,17 @@ import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.touch.client.Point; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HasEnabled; import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; import com.google.gwt.user.client.ui.ResizeComposite; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; @@ -69,6 +81,7 @@ import com.vaadin.client.renderers.ComplexRenderer; import com.vaadin.client.renderers.Renderer; import com.vaadin.client.renderers.WidgetRenderer; import com.vaadin.client.ui.SubPartAware; +import com.vaadin.client.ui.dd.DragAndDropHandler; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; @@ -78,10 +91,16 @@ import com.vaadin.client.widget.escalator.RowContainer; import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction; +import com.vaadin.client.widget.escalator.Spacer; +import com.vaadin.client.widget.escalator.SpacerUpdater; +import com.vaadin.client.widget.grid.AutoScroller; +import com.vaadin.client.widget.grid.AutoScroller.AutoScrollerCallback; +import com.vaadin.client.widget.grid.AutoScroller.ScrollAxis; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; import com.vaadin.client.widget.grid.DataAvailableEvent; import com.vaadin.client.widget.grid.DataAvailableHandler; +import com.vaadin.client.widget.grid.DetailsGenerator; import com.vaadin.client.widget.grid.EditorHandler; import com.vaadin.client.widget.grid.EditorHandler.EditorRequest; import com.vaadin.client.widget.grid.EventCellReference; @@ -95,6 +114,10 @@ import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler; import com.vaadin.client.widget.grid.events.BodyKeyDownHandler; import com.vaadin.client.widget.grid.events.BodyKeyPressHandler; import com.vaadin.client.widget.grid.events.BodyKeyUpHandler; +import com.vaadin.client.widget.grid.events.ColumnReorderEvent; +import com.vaadin.client.widget.grid.events.ColumnReorderHandler; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler; import com.vaadin.client.widget.grid.events.FooterClickHandler; import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; @@ -127,7 +150,10 @@ import com.vaadin.client.widget.grid.sort.SortEvent; import com.vaadin.client.widget.grid.sort.SortHandler; import com.vaadin.client.widget.grid.sort.SortOrder; import com.vaadin.client.widgets.Escalator.AbstractRowContainer; +import com.vaadin.client.widgets.Escalator.SubPartArguments; import com.vaadin.client.widgets.Grid.Editor.State; +import com.vaadin.client.widgets.Grid.StaticSection.StaticCell; +import com.vaadin.client.widgets.Grid.StaticSection.StaticRow; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridStaticCellType; @@ -406,6 +432,16 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns <code>true</code> if this row contains spanned cells. + * + * @since 7.5.0 + * @return does this row contain spanned cells + */ + public boolean hasSpannedCells() { + return !cellGroups.isEmpty(); + } + + /** * Merges columns cells in a row * * @param columns @@ -420,6 +456,8 @@ public class Grid<T> extends ResizeComposite implements } HashSet<Column<?, ?>> columnGroup = new HashSet<Column<?, ?>>(); + // NOTE: this doesn't care about hidden columns, those are + // filtered in calculateColspans() for (Column<?, ?> column : columns) { if (!cells.containsKey(column)) { throw new IllegalArgumentException( @@ -483,39 +521,46 @@ public class Grid<T> extends ResizeComposite implements } void calculateColspans() { - // Reset all cells for (CELLTYPE cell : this.cells.values()) { cell.setColspan(1); } - - List<Column<?, ?>> columnOrder = new ArrayList<Column<?, ?>>( - section.grid.getColumns()); // Set colspan for grouped cells for (Set<Column<?, ?>> group : cellGroups.keySet()) { - if (!checkCellGroupAndOrder(columnOrder, group)) { + if (!checkMergedCellIsContinuous(group)) { + // on error simply break the merged cell cellGroups.get(group).setColspan(1); } else { - int colSpan = group.size(); - cellGroups.get(group).setColspan(colSpan); + int colSpan = 0; + for (Column<?, ?> column : group) { + if (!column.isHidden()) { + colSpan++; + } + } + // colspan can't be 0 + cellGroups.get(group).setColspan(Math.max(1, colSpan)); } } } - private boolean checkCellGroupAndOrder( - List<Column<?, ?>> columnOrder, Set<Column<?, ?>> cellGroup) { - if (!columnOrder.containsAll(cellGroup)) { + private boolean checkMergedCellIsContinuous( + Set<Column<?, ?>> mergedCell) { + // no matter if hidden or not, just check for continuous order + final List<Column<?, ?>> columnOrder = new ArrayList<Column<?, ?>>( + section.grid.getColumns()); + + if (!columnOrder.containsAll(mergedCell)) { return false; } for (int i = 0; i < columnOrder.size(); ++i) { - if (!cellGroup.contains(columnOrder.get(i))) { + if (!mergedCell.contains(columnOrder.get(i))) { continue; } - for (int j = 1; j < cellGroup.size(); ++j) { - if (!cellGroup.contains(columnOrder.get(i + j))) { + for (int j = 1; j < mergedCell.size(); ++j) { + if (!mergedCell.contains(columnOrder.get(i + j))) { return false; } } @@ -758,6 +803,14 @@ public class Grid<T> extends ResizeComposite implements assert grid != null; return grid; } + + protected void updateColSpans() { + for (ROWTYPE row : rows) { + if (row.hasSpannedCells()) { + row.calculateColspans(); + } + } + } } /** @@ -1426,7 +1479,7 @@ public class Grid<T> extends ResizeComposite implements cellWrapper.appendChild(cell); - Column<?, T> column = grid.getColumn(i); + Column<?, T> column = grid.getVisibleColumn(i); if (column.isEditable()) { Widget editor = getHandler().getWidget(column); if (editor != null) { @@ -1788,6 +1841,14 @@ public class Grid<T> extends ResizeComposite implements private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle"; + /** + * An initial height that is given to new details rows before rendering the + * appropriate widget that we then can be measure + * + * @see GridSpacerUpdater + */ + private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; + private EventCellReference<T> eventCell = new EventCellReference<T>(this); private GridKeyDownEvent keyDown = new GridKeyDownEvent(this, eventCell); private GridKeyUpEvent keyUp = new GridKeyUpEvent(this, eventCell); @@ -1880,28 +1941,33 @@ public class Grid<T> extends ResizeComposite implements /** * Sets the currently focused. + * <p> + * <em>NOTE:</em> the column index is the index in DOM, not the logical + * column index which includes hidden columns. * - * @param row + * @param rowIndex * the index of the row having focus - * @param column - * the index of the column having focus + * @param columnIndexDOM + * the index of the cell having focus * @param container * the row container having focus */ - private void setCellFocus(int row, int column, RowContainer container) { - if (row == rowWithFocus && cellFocusRange.contains(column) + private void setCellFocus(int rowIndex, int columnIndexDOM, + RowContainer container) { + if (rowIndex == rowWithFocus + && cellFocusRange.contains(columnIndexDOM) && container == this.containerWithFocus) { refreshRow(rowWithFocus); return; } int oldRow = rowWithFocus; - rowWithFocus = row; + rowWithFocus = rowIndex; Range oldRange = cellFocusRange; if (container == escalator.getBody()) { scrollToRow(rowWithFocus); - cellFocusRange = Range.withLength(column, 1); + cellFocusRange = Range.withLength(columnIndexDOM, 1); } else { int i = 0; Element cell = container.getRowElement(rowWithFocus) @@ -1910,7 +1976,7 @@ public class Grid<T> extends ResizeComposite implements int colSpan = cell .getPropertyInt(FlyweightCell.COLSPAN_ATTR); Range cellRange = Range.withLength(i, colSpan); - if (cellRange.contains(column)) { + if (cellRange.contains(columnIndexDOM)) { cellFocusRange = cellRange; break; } @@ -1918,10 +1984,12 @@ public class Grid<T> extends ResizeComposite implements ++i; } while (cell != null); } - - if (column >= escalator.getColumnConfiguration() + int columnIndex = getColumns().indexOf( + getVisibleColumn(columnIndexDOM)); + if (columnIndex >= escalator.getColumnConfiguration() .getFrozenColumnCount()) { - escalator.scrollToColumn(column, ScrollDestination.ANY, 10); + escalator.scrollToColumn(columnIndexDOM, ScrollDestination.ANY, + 10); } if (this.containerWithFocus == container) { @@ -1967,7 +2035,7 @@ public class Grid<T> extends ResizeComposite implements * a cell object */ public void setCellFocus(CellReference<T> cell) { - setCellFocus(cell.getRowIndex(), cell.getColumnIndex(), + setCellFocus(cell.getRowIndex(), cell.getColumnIndexDOM(), escalator.findRowContainer(cell.getElement())); } @@ -2001,7 +2069,7 @@ public class Grid<T> extends ResizeComposite implements --newRow; break; case KeyCodes.KEY_RIGHT: - if (cellFocusRange.getEnd() >= getColumns().size()) { + if (cellFocusRange.getEnd() >= getVisibleColumns().size()) { return; } newColumn = cellFocusRange.getEnd(); @@ -2485,7 +2553,7 @@ public class Grid<T> extends ResizeComposite implements private boolean columnsAreGuaranteedToBeWiderThanGrid() { double freeSpace = escalator.getInnerWidth(); - for (Column<?, ?> column : getColumns()) { + for (Column<?, ?> column : getVisibleColumns()) { if (column.getWidth() >= 0) { freeSpace -= column.getWidth(); } else if (column.getMinimumWidth() >= 0) { @@ -2501,7 +2569,7 @@ public class Grid<T> extends ResizeComposite implements /* Step 1: Apply all column widths as they are. */ Map<Integer, Double> selfWidths = new LinkedHashMap<Integer, Double>(); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (int index = 0; index < columns.size(); index++) { selfWidths.put(index, columns.get(index).getWidth()); } @@ -2542,6 +2610,7 @@ public class Grid<T> extends ResizeComposite implements final Set<Column<?, T>> columnsToExpand = new HashSet<Column<?, T>>(); List<Column<?, T>> nonFixedColumns = new ArrayList<Column<?, T>>(); Map<Integer, Double> columnSizes = new HashMap<Integer, Double>(); + final List<Column<?, T>> visibleColumns = getVisibleColumns(); /* * Set all fixed widths and also calculate the size-to-fit widths @@ -2550,7 +2619,7 @@ public class Grid<T> extends ResizeComposite implements * This way we know with how many pixels we have left to expand the * rest. */ - for (Column<?, T> column : getColumns()) { + for (Column<?, T> column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; final double widthFixed = Math.max(widthAsIs, @@ -2559,11 +2628,11 @@ public class Grid<T> extends ResizeComposite implements && (column.getExpandRatio() == -1 || column == selectionColumn); if (isFixedWidth) { - columnSizes.put(indexOfColumn(column), widthFixed); + columnSizes.put(visibleColumns.indexOf(column), widthFixed); reservedPixels += widthFixed; } else { nonFixedColumns.add(column); - columnSizes.put(indexOfColumn(column), -1.0d); + columnSizes.put(visibleColumns.indexOf(column), -1.0d); } } @@ -2581,7 +2650,7 @@ public class Grid<T> extends ResizeComposite implements columnsToExpand.add(column); } reservedPixels += newWidth; - columnSizes.put(indexOfColumn(column), newWidth); + columnSizes.put(visibleColumns.indexOf(column), newWidth); } /* @@ -2609,8 +2678,8 @@ public class Grid<T> extends ResizeComposite implements final Column<?, T> column = i.next(); final int expandRatio = getExpandRatio(column, defaultExpandRatios); - final double autoWidth = columnSizes - .get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + final double autoWidth = columnSizes.get(columnIndex); final double maxWidth = getMaxWidth(column); double expandedWidth = autoWidth + widthPerRatio * expandRatio; @@ -2620,7 +2689,7 @@ public class Grid<T> extends ResizeComposite implements totalRatios -= expandRatio; aColumnHasMaxedOut = true; pixelsToDistribute -= maxWidth - autoWidth; - columnSizes.put(indexOfColumn(column), maxWidth); + columnSizes.put(columnIndex, maxWidth); } } } while (aColumnHasMaxedOut); @@ -2656,13 +2725,14 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : columnsToExpand) { final int expandRatio = getExpandRatio(column, defaultExpandRatios); - final double autoWidth = columnSizes.get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + final double autoWidth = columnSizes.get(columnIndex); double totalWidth = autoWidth + widthPerRatio * expandRatio; if (leftOver > 0) { totalWidth += 1; leftOver--; } - columnSizes.put(indexOfColumn(column), totalWidth); + columnSizes.put(columnIndex, totalWidth); totalRatios -= expandRatio; } @@ -2683,7 +2753,7 @@ public class Grid<T> extends ResizeComposite implements * remove those pixels from other columns */ double pixelsToRemoveFromOtherColumns = 0; - for (Column<?, T> column : getColumns()) { + for (Column<?, T> column : visibleColumns) { /* * We can't iterate over columnsToExpand, even though that * would be convenient. This is because some column without @@ -2692,11 +2762,11 @@ public class Grid<T> extends ResizeComposite implements */ double minWidth = getMinWidth(column); - double currentWidth = columnSizes - .get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + double currentWidth = columnSizes.get(columnIndex); boolean hasAutoWidth = column.getWidth() < 0; if (hasAutoWidth && currentWidth < minWidth) { - columnSizes.put(indexOfColumn(column), minWidth); + columnSizes.put(columnIndex, minWidth); pixelsToRemoveFromOtherColumns += (minWidth - currentWidth); minWidthsCausedReflows = true; @@ -2722,7 +2792,7 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : columnsToExpand) { final double pixelsToRemove = pixelsToRemovePerRatio * getExpandRatio(column, defaultExpandRatios); - int colIndex = indexOfColumn(column); + int colIndex = visibleColumns.indexOf(column); columnSizes.put(colIndex, columnSizes.get(colIndex) - pixelsToRemove); } @@ -2789,6 +2859,455 @@ public class Grid<T> extends ResizeComposite implements } } + private class GridSpacerUpdater implements SpacerUpdater { + + private static final String STRIPE_CLASSNAME = "stripe"; + + private final Map<Element, Widget> elementToWidgetMap = new HashMap<Element, Widget>(); + + @Override + public void init(Spacer spacer) { + initTheming(spacer); + + int rowIndex = spacer.getRow(); + + Widget detailsWidget = null; + try { + detailsWidget = detailsGenerator.getDetails(rowIndex); + } catch (Throwable e) { + getLogger().log( + Level.SEVERE, + "Exception while generating details for row " + + rowIndex, e); + } + + final double spacerHeight; + Element spacerElement = spacer.getElement(); + if (detailsWidget == null) { + spacerElement.removeAllChildren(); + spacerHeight = DETAILS_ROW_INITIAL_HEIGHT; + } else { + Element element = detailsWidget.getElement(); + spacerElement.appendChild(element); + setParent(detailsWidget, Grid.this); + Widget previousWidget = elementToWidgetMap.put(element, + detailsWidget); + + assert previousWidget == null : "Overwrote a pre-existing widget on row " + + rowIndex + " without proper removal first."; + + /* + * Once we have the content properly inside the DOM, we should + * re-measure it to make sure that it's the correct height. + * + * This is rather tricky, since the row (tr) will get the + * height, but the spacer cell (td) has the borders, which + * should go on top of the previous row and next row. + */ + double requiredHeightBoundingClientRectDouble = WidgetUtil + .getRequiredHeightBoundingClientRectDouble(element); + double borderTopAndBottomHeight = WidgetUtil + .getBorderTopAndBottomThickness(spacerElement); + double measuredHeight = requiredHeightBoundingClientRectDouble + + borderTopAndBottomHeight; + assert getElement().isOrHasChild(spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be."; + spacerHeight = measuredHeight; + } + + escalator.getBody().setSpacer(rowIndex, spacerHeight); + } + + @Override + public void destroy(Spacer spacer) { + Element spacerElement = spacer.getElement(); + + assert getElement().isOrHasChild(spacerElement) : "Trying " + + "to destroy a spacer that is not connected to this " + + "Grid's DOM. (row: " + spacer.getRow() + ", element: " + + spacerElement + ")"; + + Widget detailsWidget = elementToWidgetMap.remove(spacerElement + .getFirstChildElement()); + + if (detailsWidget != null) { + /* + * The widget may be null here if the previous generator + * returned a null widget. + */ + + assert spacerElement.getFirstChild() != null : "The " + + "details row to destroy did not contain a widget - " + + "probably removed by something else without " + + "permission? (row: " + spacer.getRow() + + ", element: " + spacerElement + ")"; + + setParent(detailsWidget, null); + spacerElement.removeAllChildren(); + } + } + + private void initTheming(Spacer spacer) { + Element spacerRoot = spacer.getElement(); + + if (spacer.getRow() % 2 == 1) { + spacerRoot.getParentElement().addClassName(STRIPE_CLASSNAME); + } else { + spacerRoot.getParentElement().removeClassName(STRIPE_CLASSNAME); + } + } + + } + + /** + * Sidebar displaying toggles for hidable columns and custom widgets + * provided by the application. + * <p> + * The button for opening the sidebar is automatically visible inside the + * grid, if it contains any column hiding options or custom widgets. The + * column hiding toggles and custom widgets become visible once the sidebar + * has been opened. + * + * @since 7.5.0 + */ + private static class Sidebar extends Composite { + + private final ClickHandler openCloseButtonHandler = new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + if (!isOpen()) { + open(); + } else { + close(); + } + } + }; + + private final FlowPanel rootContainer; + + private final FlowPanel content; + + private final MenuBar menuBar; + + private final Button openCloseButton; + + private final Grid<?> grid; + + private Sidebar(Grid<?> grid) { + this.grid = grid; + + rootContainer = new FlowPanel(); + initWidget(rootContainer); + + openCloseButton = new Button(); + openCloseButton.addClickHandler(openCloseButtonHandler); + + rootContainer.add(openCloseButton); + + content = new FlowPanel() { + @Override + public boolean remove(Widget w) { + // Check here to catch child.removeFromParent() calls + boolean removed = super.remove(w); + if (removed) { + updateVisibility(); + } + + return removed; + } + }; + + menuBar = new MenuBar(true) { + + @Override + public MenuItem insertItem(MenuItem item, int beforeIndex) + throws IndexOutOfBoundsException { + if (getParent() == null) { + content.insert(this, 0); + updateVisibility(); + } + return super.insertItem(item, beforeIndex); + } + + @Override + public void removeItem(MenuItem item) { + super.removeItem(item); + if (getItems().isEmpty()) { + menuBar.removeFromParent(); + } + } + + @Override + public void onBrowserEvent(Event event) { + // selecting a item with enter will lose the focus and + // selected item, which means that further keyboard + // selection won't work unless we do this: + if (event.getTypeInt() == Event.ONKEYDOWN + && event.getKeyCode() == KeyCodes.KEY_ENTER) { + final MenuItem item = getSelectedItem(); + super.onBrowserEvent(event); + Scheduler.get().scheduleDeferred( + new ScheduledCommand() { + + @Override + public void execute() { + selectItem(item); + focus(); + } + }); + + } else { + super.onBrowserEvent(event); + } + } + + }; + KeyDownHandler keyDownHandler = new KeyDownHandler() { + + @Override + public void onKeyDown(KeyDownEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { + close(); + } + } + }; + openCloseButton.addDomHandler(keyDownHandler, + KeyDownEvent.getType()); + menuBar.addDomHandler(keyDownHandler, KeyDownEvent.getType()); + } + + /** + * Opens the sidebar if not yet opened. Opening the sidebar has no + * effect if it is empty. + */ + public void open() { + if (!isOpen() && isInDOM()) { + addStyleName("opened"); + removeStyleName("closed"); + rootContainer.add(content); + } + openCloseButton.setHeight(""); + } + + /** + * Closes the sidebar if not yet closed. + */ + public void close() { + if (isOpen()) { + removeStyleName("opened"); + addStyleName("closed"); + content.removeFromParent(); + // adjust open button to header height when closed + setHeightToHeaderCellHeight(); + } + } + + /** + * Returns whether the sidebar is open or not. + * + * @return <code>true</code> if open, <code>false</code> if not + */ + public boolean isOpen() { + return content.getParent() == rootContainer; + } + + /** + * Adds or moves the given widget to the end of the sidebar. + * + * @param widget + * the widget to add or move + */ + public void add(Widget widget) { + content.add(widget); + updateVisibility(); + } + + /** + * Removes the given widget from the sidebar. + * + * @param widget + * the widget to remove + */ + public void remove(Widget widget) { + content.remove(widget); + // updateVisibility is called by remove listener + } + + /** + * Inserts given widget to the given index inside the sidebar. If the + * widget is already in the sidebar, then it is moved to the new index. + * <p> + * See + * {@link FlowPanel#insert(com.google.gwt.user.client.ui.IsWidget, int)} + * for further details. + * + * @param widget + * the widget to insert + * @param beforeIndex + * 0-based index position for the widget. + */ + public void insert(Widget widget, int beforeIndex) { + content.insert(widget, beforeIndex); + updateVisibility(); + } + + @Override + public void setStylePrimaryName(String styleName) { + super.setStylePrimaryName(styleName); + content.setStylePrimaryName(styleName + "-content"); + openCloseButton.setStylePrimaryName(styleName + "-button"); + if (isOpen()) { + addStyleName("open"); + removeStyleName("closed"); + } else { + removeStyleName("open"); + addStyleName("closed"); + } + } + + private void setHeightToHeaderCellHeight() { + try { + double height = WidgetUtil + .getRequiredHeightBoundingClientRectDouble(grid.escalator + .getHeader().getRowElement(0) + .getFirstChildElement()) + - (WidgetUtil.measureVerticalBorder(getElement()) / 2); + openCloseButton.setHeight(height + "px"); + } catch (NullPointerException npe) { + getLogger() + .warning( + "Got null header first row or first row cell when calculating sidebar button height"); + openCloseButton.setHeight(grid.escalator.getHeader() + .getDefaultRowHeight() + "px"); + } + } + + private void updateVisibility() { + final boolean hasWidgets = content.getWidgetCount() > 0; + final boolean isVisible = isInDOM(); + if (isVisible && !hasWidgets) { + Grid.setParent(this, null); + getElement().removeFromParent(); + } else if (!isVisible && hasWidgets) { + close(); + grid.getElement().appendChild(getElement()); + Grid.setParent(this, grid); + // border calculation won't work until attached + setHeightToHeaderCellHeight(); + } + } + + private boolean isInDOM() { + return getParent() != null; + } + + @Override + protected void onAttach() { + super.onAttach(); + // make sure the button will get correct height if the button should + // be visible when the grid is rendered the first time. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + setHeightToHeaderCellHeight(); + } + }); + } + } + + /** + * UI and functionality related to hiding columns with toggles in the + * sidebar. + */ + private final class ColumnHider { + + /** Map from columns to their hiding toggles, component might change */ + private HashMap<Column<?, T>, MenuItem> columnToHidingToggleMap = new HashMap<Grid.Column<?, T>, MenuItem>(); + + /** + * When column is being hidden with a toggle, do not refresh toggles for + * no reason. Also helps for keeping the keyboard navigation working. + */ + private boolean hidingColumn; + + private void updateColumnHidable(final Column<?, T> column) { + if (column.isHidable()) { + MenuItem toggle = columnToHidingToggleMap.get(column); + if (toggle == null) { + toggle = createToggle(column); + } + toggle.setStyleName("hidden", column.isHidden()); + } else if (columnToHidingToggleMap.containsKey(column)) { + sidebar.menuBar.removeItem((columnToHidingToggleMap + .remove(column))); + } + updateTogglesOrder(); + } + + private MenuItem createToggle(final Column<?, T> column) { + MenuItem toggle = new MenuItem(createHTML(column), true, + new ScheduledCommand() { + + @Override + public void execute() { + hidingColumn = true; + column.setHidden(!column.isHidden(), true); + hidingColumn = false; + } + }); + toggle.addStyleName("column-hiding-toggle"); + columnToHidingToggleMap.put(column, toggle); + return toggle; + } + + private String createHTML(Column<?, T> column) { + final StringBuffer buf = new StringBuffer(); + buf.append("<span class=\""); + if (column.isHidden()) { + buf.append("v-off"); + } else { + buf.append("v-on"); + } + buf.append("\"><div>"); + String caption = column.getHidingToggleCaption(); + if (caption == null) { + caption = column.headerCaption; + } + buf.append(caption); + buf.append("</div></span>"); + + return buf.toString(); + } + + private void updateTogglesOrder() { + if (!hidingColumn) { + int lastIndex = 0; + for (Column<?, T> column : getColumns()) { + if (column.isHidable()) { + final MenuItem menuItem = columnToHidingToggleMap + .get(column); + sidebar.menuBar.removeItem(menuItem); + sidebar.menuBar.insertItem(menuItem, lastIndex++); + } + } + } + } + + private void updateHidingToggle(Column<?, T> column) { + if (column.isHidable()) { + MenuItem toggle = columnToHidingToggleMap.get(column); + toggle.setHTML(createHTML(column)); + toggle.setStyleName("hidden", column.isHidden()); + } // else we can just ignore + } + + private void removeColumnHidingToggle(Column<?, T> column) { + sidebar.menuBar.removeItem(columnToHidingToggleMap.get(column)); + } + + } + /** * Escalator used internally by grid to render the rows */ @@ -2798,6 +3317,8 @@ public class Grid<T> extends ResizeComposite implements private final Footer footer = GWT.create(Footer.class); + private final Sidebar sidebar = new Sidebar(this); + /** * List of columns in the grid. Order defines the visible order. */ @@ -2874,6 +3395,442 @@ public class Grid<T> extends ResizeComposite implements private boolean enabled = true; + private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; + private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater(); + /** A set keeping track of the indices of all currently open details */ + private Set<Integer> visibleDetails = new HashSet<Integer>(); + + private boolean columnReorderingAllowed; + + private ColumnHider columnHider = new ColumnHider(); + + private DragAndDropHandler dndHandler = new DragAndDropHandler(); + + private AutoScroller autoScroller = new AutoScroller(this); + + private DragAndDropHandler.DragAndDropCallback headerCellDndCallback = new DragAndDropHandler.DragAndDropCallback() { + + private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { + + @Override + public void onAutoScroll(int scrollDiff) { + autoScrollX = scrollDiff; + onDragUpdate(null); + } + + @Override + public void onAutoScrollReachedMin() { + // make sure the drop marker is visible on the left + autoScrollX = 0; + updateDragDropMarker(clientX); + } + + @Override + public void onAutoScrollReachedMax() { + // make sure the drop marker is visible on the right + autoScrollX = 0; + updateDragDropMarker(clientX); + } + }; + /** + * Elements for displaying the dragged column(s) and drop marker + * properly + */ + private Element table; + private Element tableHeader; + /** Marks the column drop location */ + private Element dropMarker; + /** A copy of the dragged column(s), moves with cursor. */ + private Element dragElement; + /** Tracks index of the column whose left side the drop would occur */ + private int latestColumnDropIndex; + /** + * Map of possible drop positions for the column and the corresponding + * column index. + */ + private final TreeMap<Double, Integer> possibleDropPositions = new TreeMap<Double, Integer>(); + /** + * Makes sure that drag cancel doesn't cause anything unwanted like sort + */ + private HandlerRegistration columnSortPreventRegistration; + + private int clientX; + + /** How much the grid is being auto scrolled while dragging. */ + private int autoScrollX; + + /** Captures the value of the focused column before reordering */ + private int focusedColumnIndex; + + private void initHeaderDragElementDOM() { + if (table == null) { + tableHeader = DOM.createTHead(); + dropMarker = DOM.createDiv(); + tableHeader.appendChild(dropMarker); + table = DOM.createTable(); + table.appendChild(tableHeader); + table.setClassName("header-drag-table"); + } + // update the style names on each run in case primary name has been + // modified + tableHeader.setClassName(escalator.getHeader().getElement() + .getClassName()); + dropMarker.setClassName(getStylePrimaryName() + "-drop-marker"); + int topOffset = 0; + for (int i = 0; i < eventCell.getRowIndex(); i++) { + topOffset += escalator.getHeader().getRowElement(i) + .getFirstChildElement().getOffsetHeight(); + } + tableHeader.getStyle().setTop(topOffset, Unit.PX); + + getElement().appendChild(table); + } + + @Override + public void onDragUpdate(NativePreviewEvent event) { + if (event != null) { + clientX = WidgetUtil.getTouchOrMouseClientX(event + .getNativeEvent()); + autoScrollX = 0; + } + resolveDragElementHorizontalPosition(clientX); + updateDragDropMarker(clientX); + } + + private void updateDragDropMarker(final int clientX) { + final double scrollLeft = getScrollLeft(); + final double cursorXCoordinate = clientX + - escalator.getHeader().getElement().getAbsoluteLeft(); + final Entry<Double, Integer> cellEdgeOnRight = possibleDropPositions + .ceilingEntry(cursorXCoordinate); + final Entry<Double, Integer> cellEdgeOnLeft = possibleDropPositions + .floorEntry(cursorXCoordinate); + final double diffToRightEdge = cellEdgeOnRight == null ? Double.MAX_VALUE + : cellEdgeOnRight.getKey() - cursorXCoordinate; + final double diffToLeftEdge = cellEdgeOnLeft == null ? Double.MAX_VALUE + : cursorXCoordinate - cellEdgeOnLeft.getKey(); + + double dropMarkerLeft = 0 - scrollLeft; + if (diffToRightEdge > diffToLeftEdge) { + latestColumnDropIndex = cellEdgeOnLeft.getValue(); + dropMarkerLeft += cellEdgeOnLeft.getKey(); + } else { + latestColumnDropIndex = cellEdgeOnRight.getValue(); + dropMarkerLeft += cellEdgeOnRight.getKey(); + } + + dropMarkerLeft += autoScrollX; + + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (dropMarkerLeft < frozenColumnsWidth + || dropMarkerLeft > escalator.getHeader().getElement() + .getOffsetWidth() || dropMarkerLeft < 0) { + dropMarkerLeft = -10000000; + } + dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); + } + + private void resolveDragElementHorizontalPosition(final int clientX) { + double left = clientX - table.getAbsoluteLeft(); + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (left < frozenColumnsWidth) { + left = (int) frozenColumnsWidth; + } + + // do not show the drag element beyond a spanned header cell + // limitation + final Double leftBound = possibleDropPositions.firstKey(); + final Double rightBound = possibleDropPositions.lastKey(); + double scrollLeft = getScrollLeft(); + if (left + scrollLeft < leftBound) { + left = leftBound - scrollLeft + autoScrollX; + } else if (left + scrollLeft > rightBound) { + left = rightBound - scrollLeft + autoScrollX; + } + + // do not show the drag element beyond the grid + left = Math.max(0, Math.min(left, table.getClientWidth())); + + left -= dragElement.getClientWidth() / 2; + dragElement.getStyle().setLeft(left, Unit.PX); + } + + @Override + public boolean onDragStart(NativeEvent startingEvent) { + calculatePossibleDropPositions(); + + if (possibleDropPositions.isEmpty()) { + return false; + } + + initHeaderDragElementDOM(); + // needs to clone focus and sorting indicators too (UX) + dragElement = DOM.clone(eventCell.getElement(), true); + dragElement.getStyle().clearWidth(); + dropMarker.getStyle().setProperty("height", + dragElement.getStyle().getHeight()); + tableHeader.appendChild(dragElement); + // mark the column being dragged for styling + eventCell.getElement().addClassName("dragged"); + // mark the floating cell, for styling & testing + dragElement.addClassName("dragged-column-header"); + + // start the auto scroll handler + autoScroller.setScrollArea(60); + autoScroller.start(startingEvent, ScrollAxis.HORIZONTAL, + autoScrollerCallback); + return true; + } + + @Override + public void onDragEnd() { + table.removeFromParent(); + dragElement.removeFromParent(); + eventCell.getElement().removeClassName("dragged"); + } + + @Override + public void onDrop() { + final int draggedColumnIndex = eventCell.getColumnIndex(); + final int colspan = header.getRow(eventCell.getRowIndex()) + .getCell(eventCell.getColumn()).getColspan(); + if (latestColumnDropIndex != draggedColumnIndex + && latestColumnDropIndex != (draggedColumnIndex + colspan)) { + List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> reordered = new ArrayList<Column<?, T>>(); + if (draggedColumnIndex < latestColumnDropIndex) { + reordered.addAll(columns.subList(0, draggedColumnIndex)); + reordered.addAll(columns.subList(draggedColumnIndex + + colspan, latestColumnDropIndex)); + reordered.addAll(columns.subList(draggedColumnIndex, + draggedColumnIndex + colspan)); + reordered.addAll(columns.subList(latestColumnDropIndex, + columns.size())); + } else { + reordered.addAll(columns.subList(0, latestColumnDropIndex)); + reordered.addAll(columns.subList(draggedColumnIndex, + draggedColumnIndex + colspan)); + reordered.addAll(columns.subList(latestColumnDropIndex, + draggedColumnIndex)); + reordered.addAll(columns.subList(draggedColumnIndex + + colspan, columns.size())); + } + reordered.remove(selectionColumn); // since setColumnOrder will + // add it anyway! + + // capture focused cell column before reorder + Cell focusedCell = cellFocusHandler.getFocusedCell(); + if (focusedCell != null) { + // take hidden columns into account + focusedColumnIndex = getColumns().indexOf( + getVisibleColumn(focusedCell.getColumn())); + } + + Column<?, T>[] array = reordered.toArray(new Column[reordered + .size()]); + setColumnOrder(array); + transferCellFocusOnDrop(); + } // else no reordering + } + + private void transferCellFocusOnDrop() { + final Cell focusedCell = cellFocusHandler.getFocusedCell(); + if (focusedCell != null) { + final int focusedColumnIndexDOM = focusedCell.getColumn(); + final int focusedRowIndex = focusedCell.getRow(); + final int draggedColumnIndex = eventCell.getColumnIndex(); + // transfer focus if it was effected by the new column order + final RowContainer rowContainer = escalator + .findRowContainer(focusedCell.getElement()); + if (focusedColumnIndex == draggedColumnIndex) { + // move with the dragged column + int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1 + : latestColumnDropIndex; + // remove hidden columns from indexing + adjustedDropIndex = getVisibleColumns().indexOf( + getColumn(adjustedDropIndex)); + cellFocusHandler.setCellFocus(focusedRowIndex, + adjustedDropIndex, rowContainer); + } else if (latestColumnDropIndex <= focusedColumnIndex + && draggedColumnIndex > focusedColumnIndex) { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedColumnIndexDOM + 1, rowContainer); + } else if (latestColumnDropIndex > focusedColumnIndex + && draggedColumnIndex < focusedColumnIndex) { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedColumnIndexDOM - 1, rowContainer); + } + } + } + + @Override + public void onDragCancel() { + // cancel next click so that we may prevent column sorting if + // mouse was released on top of the dragged cell + if (columnSortPreventRegistration == null) { + columnSortPreventRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent( + NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONCLICK) { + event.cancel(); + event.getNativeEvent().preventDefault(); + columnSortPreventRegistration + .removeHandler(); + columnSortPreventRegistration = null; + } + } + }); + } + autoScroller.stop(); + } + + private double getFrozenColumnsWidth() { + double value = getMultiSelectColumnWidth(); + for (int i = 0; i < getFrozenColumnCount(); i++) { + value += getColumn(i).getWidthActual(); + } + return value; + } + + private double getMultiSelectColumnWidth() { + if (getSelectionModel().getSelectionColumnRenderer() != null) { + // frozen checkbox column is present, it is always the first + // column + return escalator.getHeader().getElement() + .getFirstChildElement().getFirstChildElement() + .getOffsetWidth(); + } + return 0.0; + } + + /** + * Returns the amount of frozen columns. The selection column is always + * considered frozen, since it can't be moved. + */ + private int getSelectionAndFrozenColumnCount() { + // no matter if selection column is frozen or not, it is considered + // frozen for column dnd reorder + if (getSelectionModel().getSelectionColumnRenderer() != null) { + return Math.max(0, getFrozenColumnCount()) + 1; + } else { + return Math.max(0, getFrozenColumnCount()); + } + } + + @SuppressWarnings("boxing") + private void calculatePossibleDropPositions() { + possibleDropPositions.clear(); + + final int draggedColumnIndex = eventCell.getColumnIndex(); + final StaticRow<?> draggedCellRow = header.getRow(eventCell + .getRowIndex()); + final int draggedColumnRightIndex = draggedColumnIndex + + draggedCellRow.getCell(eventCell.getColumn()) + .getColspan(); + final int frozenColumns = getSelectionAndFrozenColumnCount(); + final Range draggedCellRange = Range.between(draggedColumnIndex, + draggedColumnRightIndex); + /* + * If the dragged cell intersects with a spanned cell in any other + * header or footer row, then the drag is limited inside that + * spanned cell. The same rules apply: the cell can't be dropped + * inside another spanned cell. The left and right bounds keep track + * of the edges of the most limiting spanned cell. + */ + int leftBound = -1; + int rightBound = getColumnCount() + 1; + + final HashSet<Integer> unavailableColumnDropIndices = new HashSet<Integer>(); + final List<StaticRow<?>> rows = new ArrayList<StaticRow<?>>(); + rows.addAll(header.getRows()); + rows.addAll(footer.getRows()); + for (StaticRow<?> row : rows) { + if (!row.hasSpannedCells()) { + continue; + } + final boolean isDraggedCellRow = row.equals(draggedCellRow); + for (int cellColumnIndex = frozenColumns; cellColumnIndex < getColumnCount(); cellColumnIndex++) { + StaticCell cell = row.getCell(getColumn(cellColumnIndex)); + int colspan = cell.getColspan(); + if (colspan <= 1) { + continue; + } + final int cellColumnRightIndex = cellColumnIndex + colspan; + final Range cellRange = Range.between(cellColumnIndex, + cellColumnRightIndex); + final boolean intersects = draggedCellRange + .intersects(cellRange); + if (intersects && !isDraggedCellRow) { + // if the currently iterated cell is inside or same as + // the dragged cell, then it doesn't restrict the drag + if (cellRange.isSubsetOf(draggedCellRange)) { + cellColumnIndex = cellColumnRightIndex - 1; + continue; + } + /* + * if the dragged cell is a spanned cell and it crosses + * with the currently iterated cell without sharing + * either start or end then not possible to drag the + * cell. + */ + if (!draggedCellRange.isSubsetOf(cellRange)) { + return; + } + // the spanned cell overlaps the dragged cell (but is + // not the dragged cell) + if (cellColumnIndex <= draggedColumnIndex + && cellColumnIndex > leftBound) { + leftBound = cellColumnIndex; + } + if (cellColumnRightIndex < rightBound) { + rightBound = cellColumnRightIndex; + } + cellColumnIndex = cellColumnRightIndex - 1; + } + + else { // can't drop inside a spanned cell, or this is the + // dragged cell + while (colspan > 1) { + cellColumnIndex++; + colspan--; + unavailableColumnDropIndices.add(cellColumnIndex); + } + } + } + } + + if (leftBound == (rightBound - 1)) { + return; + } + + double position = getFrozenColumnsWidth(); + // iterate column indices and add possible drop positions + for (int i = frozenColumns; i < getColumnCount(); i++) { + Column<?, T> column = getColumn(i); + if (!unavailableColumnDropIndices.contains(i) + && !column.isHidden()) { + if (leftBound != -1) { + if (i >= leftBound && i <= rightBound) { + possibleDropPositions.put(position, i); + } + } else { + possibleDropPositions.put(position, i); + } + } + position += column.getWidthActual(); + } + + if (leftBound == -1) { + // add the right side of the last column as columns.size() + possibleDropPositions.put(position, getColumnCount()); + } + } + + }; + /** * Enumeration for easy setting of selection mode. */ @@ -2976,8 +3933,14 @@ public class Grid<T> extends ResizeComposite implements private boolean editable = true; + private boolean hidden = false; + + private boolean hidable = false; + private String headerCaption = ""; + private String hidingToggleCaption = null; + private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; private double maximumWidthPx = GridConstants.DEFAULT_MAX_WIDTH; private int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO; @@ -3086,6 +4049,9 @@ public class Grid<T> extends ResizeComposite implements HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { row.getCell(this).setText(headerCaption); + if (isHidable()) { + grid.columnHider.updateHidingToggle(this); + } } } @@ -3148,6 +4114,9 @@ public class Grid<T> extends ResizeComposite implements * This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. + * <p> + * If the column is currently {@link #isHidden() hidden}, then this set + * width has effect only once the column has been made visible again. * * @param pixels * the width in pixels or negative for auto sizing @@ -3155,14 +4124,17 @@ public class Grid<T> extends ResizeComposite implements public Column<C, T> setWidth(double pixels) { if (!WidgetUtil.pixelValuesEqual(widthUser, pixels)) { widthUser = pixels; - scheduleColumnWidthRecalculator(); + if (!isHidden()) { + scheduleColumnWidthRecalculator(); + } } return this; } void doSetWidth(double pixels) { + assert !isHidden() : "applying width for a hidden column"; if (grid != null) { - int index = grid.columns.indexOf(this); + int index = grid.getVisibleColumns().indexOf(this); ColumnConfiguration conf = grid.escalator .getColumnConfiguration(); conf.setColumnWidth(index, pixels); @@ -3174,6 +4146,9 @@ public class Grid<T> extends ResizeComposite implements * <p> * <em>Note:</em> If a negative value was given to * {@link #setWidth(double)}, that same negative value is returned here. + * <p> + * <em>Note:</em> Returns the value, even if the column is currently + * {@link #isHidden() hidden}. * * @return pixel width of the column, or a negative number if the column * width has been automatically calculated. @@ -3188,13 +4163,18 @@ public class Grid<T> extends ResizeComposite implements * Returns the effective pixel width of the column. * <p> * This differs from {@link #getWidth()} only when the column has been - * automatically resized. + * automatically resized, or when the column is currently + * {@link #isHidden() hidden}, when the value is 0. * * @return pixel width of the column. */ public double getWidthActual() { + if (isHidden()) { + return 0; + } return grid.escalator.getColumnConfiguration() - .getColumnWidthActual(grid.columns.indexOf(this)); + .getColumnWidthActual( + grid.getVisibleColumns().indexOf(this)); } void reapplyWidth() { @@ -3231,6 +4211,129 @@ public class Grid<T> extends ResizeComposite implements return sortable; } + /** + * Hides or shows the column. By default columns are visible before + * explicitly hiding them. + * + * @since 7.5.0 + * @param hidden + * <code>true</code> to hide the column, <code>false</code> + * to show + */ + public void setHidden(boolean hidden) { + setHidden(hidden, false); + } + + private void setHidden(boolean hidden, boolean userOriginated) { + if (this.hidden != hidden) { + if (hidden) { + grid.escalator.getColumnConfiguration().removeColumns( + grid.getVisibleColumns().indexOf(this), 1); + this.hidden = hidden; + } else { + this.hidden = hidden; + + final int columnIndex = grid.getVisibleColumns().indexOf( + this); + grid.escalator.getColumnConfiguration().insertColumns( + columnIndex, 1); + + // make sure column is set to frozen if it needs to be, + // escalator doesn't handle situation where the added column + // would be the last frozen column + int gridFrozenColumns = grid.getFrozenColumnCount(); + int escalatorFrozenColumns = grid.escalator + .getColumnConfiguration().getFrozenColumnCount(); + if (gridFrozenColumns > escalatorFrozenColumns + && escalatorFrozenColumns == columnIndex) { + grid.escalator.getColumnConfiguration() + .setFrozenColumnCount(++escalatorFrozenColumns); + } + } + grid.columnHider.updateHidingToggle(this); + grid.header.updateColSpans(); + grid.footer.updateColSpans(); + scheduleColumnWidthRecalculator(); + this.grid.fireEvent(new ColumnVisibilityChangeEvent<T>(this, + hidden, userOriginated)); + } + } + + /** + * Is this column hidden. Default is {@code false}. + * + * @since 7.5.0 + * @return <code>true</code> if the column is currently hidden, + * <code>false</code> otherwise + */ + public boolean isHidden() { + return hidden; + } + + /** + * Set whether it is possible for the user to hide this column or not. + * Default is {@code false}. + * <p> + * <em>Note:</em> it is still possible to hide the column + * programmatically using {@link #setHidden(boolean)}. + * + * @since 7.5.0 + * @param hidable + * <code>true</code> if the user can hide this column, + * <code>false</code> if not + */ + public void setHidable(boolean hidable) { + if (this.hidable != hidable) { + this.hidable = hidable; + grid.columnHider.updateColumnHidable(this); + } + } + + /** + * Is it possible for the the user to hide this column. Default is + * {@code false}. + * <p> + * <em>Note:</em> the column can be programmatically hidden using + * {@link #setHidden(boolean)} regardless of the returned value. + * + * @since 7.5.0 + * @return <code>true</code> if the user can hide the column, + * <code>false</code> if not + */ + public boolean isHidable() { + return hidable; + } + + /** + * Sets the hiding toggle's caption for this column. Shown in the toggle + * for this column in the grid's sidebar when the column is + * {@link #isHidable() hidable}. + * <p> + * Defaults to <code>null</code>, when will use whatever is set with + * {@link #setHeaderCaption(String)}. + * + * @since + * @param hidingToggleCaption + * the caption for the hiding toggle for this column + */ + public void setHidingToggleCaption(String hidingToggleCaption) { + this.hidingToggleCaption = hidingToggleCaption; + if (isHidable()) { + grid.columnHider.updateHidingToggle(this); + } + } + + /** + * Gets the hiding toggle caption for this column. + * + * @since + * @see #setHidingToggleCaption(String) + * @return the hiding toggle's caption for this column + */ + public String getHidingToggleCaption() { + return hidingToggleCaption; + } + @Override public String toString() { String details = ""; @@ -3464,8 +4567,9 @@ public class Grid<T> extends ResizeComposite implements Renderer<?> renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { + Column<?, T> column = getVisibleColumn(cell.getColumn()); rendererCellReference.set(cell, - getColumn(cell.getColumn())); + getColumns().indexOf(column), column); ((ComplexRenderer<?>) renderer) .init(rendererCellReference); } catch (RuntimeException e) { @@ -3561,7 +4665,8 @@ public class Grid<T> extends ResizeComposite implements cellFocusHandler.updateFocusedRowStyle(row); for (FlyweightCell cell : cellsToUpdate) { - Column<?, T> column = getColumn(cell.getColumn()); + Column<?, T> column = getVisibleColumn(cell.getColumn()); + final int columnIndex = getColumns().indexOf(column); assert column != null : "Column was not found from cell (" + cell.getColumn() + "," + cell.getRow() + ")"; @@ -3571,7 +4676,8 @@ public class Grid<T> extends ResizeComposite implements if (hasData && cellStyleGenerator != null) { try { - cellReference.set(cell.getColumn(), column); + cellReference + .set(cell.getColumn(), columnIndex, column); String generatedStyle = cellStyleGenerator .getStyle(cellReference); setCustomStyleName(cell.getElement(), generatedStyle); @@ -3588,7 +4694,7 @@ public class Grid<T> extends ResizeComposite implements Renderer renderer = column.getRenderer(); try { - rendererCellReference.set(cell, column); + rendererCellReference.set(cell, columnIndex, column); if (renderer instanceof ComplexRenderer) { // Hide cell content if needed ComplexRenderer clxRenderer = (ComplexRenderer) renderer; @@ -3662,8 +4768,9 @@ public class Grid<T> extends ResizeComposite implements Renderer renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { + Column<?, T> column = getVisibleColumn(cell.getColumn()); rendererCellReference.set(cell, - getColumn(cell.getColumn())); + getColumns().indexOf(column), column); ((ComplexRenderer) renderer) .destroy(rendererCellReference); } catch (RuntimeException e) { @@ -3692,7 +4799,7 @@ public class Grid<T> extends ResizeComposite implements @Override public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) { StaticSection.StaticRow<?> staticRow = section.getRow(row.getRow()); - final List<Column<?, T>> columns = getColumns(); + final List<Column<?, T>> columns = getVisibleColumns(); setCustomStyleName(row.getElement(), staticRow.getStyleName()); @@ -3740,7 +4847,7 @@ public class Grid<T> extends ResizeComposite implements cleanup(cell); - Column<?, ?> column = getColumn(cell.getColumn()); + Column<?, ?> column = getVisibleColumn(cell.getColumn()); SortOrder sortingOrder = getSortOrder(column); if (!headerRow.isDefault() || !column.isSortable() || sortingOrder == null) { @@ -3813,7 +4920,7 @@ public class Grid<T> extends ResizeComposite implements @Override public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) { StaticSection.StaticRow<?> gridRow = section.getRow(row.getRow()); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (FlyweightCell cell : attachedCells) { StaticSection.StaticCell metadata = gridRow.getCell(columns @@ -3843,7 +4950,7 @@ public class Grid<T> extends ResizeComposite implements if (section.getRowCount() > row.getRow()) { StaticSection.StaticRow<?> gridRow = section.getRow(row .getRow()); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (FlyweightCell cell : cellsToDetach) { StaticSection.StaticCell metadata = gridRow.getCell(columns .get(cell.getColumn())); @@ -3892,6 +4999,8 @@ public class Grid<T> extends ResizeComposite implements setSelectionMode(SelectionMode.SINGLE); + escalator.getBody().setSpacerUpdater(gridSpacerUpdater); + escalator.addScrollHandler(new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { @@ -3980,6 +5089,8 @@ public class Grid<T> extends ResizeComposite implements super.setStylePrimaryName(style); escalator.setStylePrimaryName(style); editor.setStylePrimaryName(style); + sidebar.setStylePrimaryName(style + "-sidebar"); + sidebar.addStyleName("v-contextmenu"); String rowStyle = getStylePrimaryName() + "-row"; rowHasDataStyleName = rowStyle + "-has-data"; @@ -4156,6 +5267,10 @@ public class Grid<T> extends ResizeComposite implements Set<String> events = new HashSet<String>(); events.addAll(getConsumedEventsForRenderer(column.getRenderer())); + if (column.isHidable()) { + columnHider.updateColumnHidable(column); + } + sinkEvents(events); } @@ -4179,7 +5294,7 @@ public class Grid<T> extends ResizeComposite implements } private Renderer<?> findRenderer(FlyweightCell cell) { - Column<?, T> column = getColumn(cell.getColumn()); + Column<?, T> column = getVisibleColumn(cell.getColumn()); assert column != null : "Could not find column at index:" + cell.getColumn(); return column.getRenderer(); @@ -4204,7 +5319,8 @@ public class Grid<T> extends ResizeComposite implements int columnIndex = columns.indexOf(column); // Remove from column configuration - escalator.getColumnConfiguration().removeColumns(columnIndex, 1); + escalator.getColumnConfiguration().removeColumns( + getVisibleColumns().indexOf(column), 1); updateFrozenColumns(); @@ -4215,10 +5331,16 @@ public class Grid<T> extends ResizeComposite implements ((Column<?, T>) column).setGrid(null); columns.remove(columnIndex); + + if (column.isHidable()) { + columnHider.removeColumnHidingToggle(column); + } } /** * Returns the amount of columns in the grid. + * <p> + * <em>NOTE:</em> this includes the hidden columns in the count. * * @return The number of columns in the grid */ @@ -4227,7 +5349,9 @@ public class Grid<T> extends ResizeComposite implements } /** - * Returns a list of columns in the grid. + * Returns a list columns in the grid, including hidden columns. + * <p> + * For currently visible columns, use {@link #getVisibleColumns()}. * * @return A unmodifiable list of the columns in the grid */ @@ -4237,7 +5361,27 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns a list of the currently visible columns in the grid. + * <p> + * No {@link Column#isHidden() hidden} columns included. + * + * @since 7.5.0 + * @return A unmodifiable list of the currently visible columns in the grid + */ + public List<Column<?, T>> getVisibleColumns() { + ArrayList<Column<?, T>> visible = new ArrayList<Column<?, T>>(); + for (Column<?, T> c : columns) { + if (!c.isHidden()) { + visible.add(c); + } + } + return Collections.unmodifiableList(visible); + } + + /** * Returns a column by its index in the grid. + * <p> + * <em>NOTE:</em> The indexing includes hidden columns. * * @param index * the index of the column @@ -4252,15 +5396,13 @@ public class Grid<T> extends ResizeComposite implements return columns.get(index); } - /** - * Returns current index of given column - * - * @param column - * column in grid - * @return column index, or <code>-1</code> if not in this Grid - */ - protected int indexOfColumn(Column<?, T> column) { - return columns.indexOf(column); + private Column<?, T> getVisibleColumn(int index) + throws IllegalArgumentException { + List<Column<?, T>> visibleColumns = getVisibleColumns(); + if (index < 0 || index >= visibleColumns.size()) { + throw new IllegalStateException("Column not found."); + } + return visibleColumns.get(index); } /** @@ -4708,13 +5850,21 @@ public class Grid<T> extends ResizeComposite implements + getColumnCount() + ")"); } - this.frozenColumnCount = numberOfColumns; + frozenColumnCount = numberOfColumns; updateFrozenColumns(); } private void updateFrozenColumns() { int numberOfColumns = frozenColumnCount; + // for the escalator the hidden columns are not in the frozen column + // count, but for grid they are. thus need to convert the index + for (int i = 0; i < frozenColumnCount; i++) { + if (getColumn(i).isHidden()) { + numberOfColumns--; + } + } + if (numberOfColumns == -1) { numberOfColumns = 0; } else if (selectionColumn != null) { @@ -4730,6 +5880,9 @@ public class Grid<T> extends ResizeComposite implements * columns will be frozen, but the built-in selection checkbox column will * still be frozen if it's in use. -1 means that not even the selection * column is frozen. + * <p> + * <em>NOTE:</em> This includes {@link Column#isHidden() hidden columns} in + * the count. * * @return the number of frozen columns */ @@ -4749,6 +5902,9 @@ public class Grid<T> extends ResizeComposite implements /** * Scrolls to a certain row, using {@link ScrollDestination#ANY}. + * <p> + * If the details for that row are visible, those will be taken into account + * as well. * * @param rowIndex * zero-based index of the row to scroll to. @@ -4763,6 +5919,9 @@ public class Grid<T> extends ResizeComposite implements /** * Scrolls to a certain row, using user-specified scroll destination. + * <p> + * If the details for that row are visible, those will be taken into account + * as well. * * @param rowIndex * zero-based index of the row to scroll to. @@ -4782,6 +5941,9 @@ public class Grid<T> extends ResizeComposite implements /** * Scrolls to a certain row using only user-specified parameters. + * <p> + * If the details for that row are visible, those will be taken into account + * as well. * * @param rowIndex * zero-based index of the row to scroll to. @@ -4811,7 +5973,7 @@ public class Grid<T> extends ResizeComposite implements + ") is above maximum (" + maxsize + ")!"); } - escalator.scrollToRow(rowIndex, destination, paddingPx); + escalator.scrollToRowAndSpacer(rowIndex, destination, paddingPx); } /** @@ -4849,6 +6011,17 @@ public class Grid<T> extends ResizeComposite implements } /** + * Sets the horizontal scroll offset + * + * @since 7.5.0 + * @param px + * the number of pixels this grid should be scrolled right + */ + public void setScrollLeft(double px) { + escalator.setScrollLeft(px); + } + + /** * Gets the horizontal scroll offset * * @return the number of pixels this grid is scrolled to the right @@ -4857,6 +6030,26 @@ public class Grid<T> extends ResizeComposite implements return escalator.getScrollLeft(); } + /** + * Returns the height of the scrollable area in pixels. + * + * @since 7.5.0 + * @return the height of the scrollable area in pixels + */ + public double getScrollHeight() { + return escalator.getScrollHeight(); + } + + /** + * Returns the width of the scrollable area in pixels. + * + * @since 7.5.0 + * @return the width of the scrollable area in pixels. + */ + public double getScrollWidth() { + return escalator.getScrollWidth(); + } + private static final Logger getLogger() { return Logger.getLogger(Grid.class.getName()); } @@ -4960,7 +6153,7 @@ public class Grid<T> extends ResizeComposite implements EventTarget target = event.getEventTarget(); - if (!Element.is(target)) { + if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) { return; } @@ -5007,6 +6200,10 @@ public class Grid<T> extends ResizeComposite implements if (!isElementInChildWidget(e)) { + if (handleHeaderCellDragStartEvent(event, container)) { + return; + } + // Sorting through header Click / KeyUp if (handleHeaderDefaultRowEvent(event, container)) { return; @@ -5026,6 +6223,21 @@ public class Grid<T> extends ResizeComposite implements } } + private boolean isOrContainsInSpacer(Node node) { + Node n = node; + while (n != null && n != getElement()) { + boolean isElement = Element.is(n); + if (isElement) { + String className = Element.as(n).getClassName(); + if (className.contains(getStylePrimaryName() + "-spacer")) { + return true; + } + } + n = n.getParentNode(); + } + return false; + } + private boolean isElementInChildWidget(Element e) { Widget w = WidgetUtil.findWidget(e, null); @@ -5155,6 +6367,31 @@ public class Grid<T> extends ResizeComposite implements return true; } + private boolean handleHeaderCellDragStartEvent(Event event, + RowContainer container) { + if (!isColumnReorderingAllowed()) { + return false; + } + if (container != escalator.getHeader()) { + return false; + } + if (eventCell.getColumnIndex() < escalator.getColumnConfiguration() + .getFrozenColumnCount()) { + return false; + } + + if (event.getTypeInt() == Event.ONMOUSEDOWN + && event.getButton() == NativeEvent.BUTTON_LEFT + || event.getTypeInt() == Event.ONTOUCHSTART) { + dndHandler.onDragStartOnDraggableElement(event, + headerCellDndCallback); + event.preventDefault(); + event.stopPropagation(); + return true; + } + return false; + } + private Point rowEventTouchStartingPoint; private CellStyleGenerator<T> cellStyleGenerator; private RowStyleGenerator<T> rowStyleGenerator; @@ -5253,153 +6490,82 @@ public class Grid<T> extends ResizeComposite implements } @Override + @SuppressWarnings("deprecation") public com.google.gwt.user.client.Element getSubPartElement(String subPart) { - // Parse SubPart string to type and indices - String[] splitArgs = subPart.split("\\["); - - String type = splitArgs[0]; - int[] indices = new int[splitArgs.length - 1]; - for (int i = 0; i < indices.length; ++i) { - String tmp = splitArgs[i + 1]; - indices[i] = Integer.parseInt(tmp.substring(0, tmp.length() - 1)); - } - - // Get correct RowContainer for type from Escalator - RowContainer container = null; - if (type.equalsIgnoreCase("header")) { - container = escalator.getHeader(); - } else if (type.equalsIgnoreCase("cell")) { - // If wanted row is not visible, we need to scroll there. - Range visibleRowRange = escalator.getVisibleRowRange(); - if (indices.length > 0 && !visibleRowRange.contains(indices[0])) { - try { - scrollToRow(indices[0]); - } catch (IllegalArgumentException e) { - getLogger().log(Level.SEVERE, e.getMessage()); - } - // Scrolling causes a lazy loading event. No element can - // currently be retrieved. - return null; - } - container = escalator.getBody(); - } else if (type.equalsIgnoreCase("footer")) { - container = escalator.getFooter(); - } else if (type.equalsIgnoreCase("editor")) { - if (editor.getState() != State.ACTIVE) { - // Editor is not there. - return null; - } - if (indices.length == 0) { - return DOM.asOld(editor.editorOverlay); - } else if (indices.length == 1 && indices[0] < columns.size()) { - escalator.scrollToColumn(indices[0], ScrollDestination.ANY, 0); - return editor.getWidget(columns.get(indices[0])).getElement(); - } else { - return null; - } - } + /* + * handles details[] (translated to spacer[] for Escalator), cell[], + * header[] and footer[] + */ + Element escalatorElement = escalator.getSubPartElement(subPart + .replaceFirst("^details\\[", "spacer[")); - if (null != container) { - if (indices.length == 0) { - // No indexing. Just return the wanted container element - return DOM.asOld(container.getElement()); - } else { - try { - return DOM.asOld(getSubPart(container, indices)); - } catch (Exception e) { - getLogger().log(Level.SEVERE, e.getMessage()); - } - } + if (escalatorElement != null) { + return DOM.asOld(escalatorElement); } - return null; - } - private Element getSubPart(RowContainer container, int[] indices) { - Element targetElement = container.getRowElement(indices[0]); - - // Scroll wanted column to view if able - if (indices.length > 1 && targetElement != null) { - if (escalator.getColumnConfiguration().getFrozenColumnCount() <= indices[1]) { - escalator.scrollToColumn(indices[1], ScrollDestination.ANY, 0); - } + SubPartArguments args = Escalator.parseSubPartArguments(subPart); - targetElement = getCellFromRow(TableRowElement.as(targetElement), - indices[1]); - - for (int i = 2; i < indices.length && targetElement != null; ++i) { - targetElement = (Element) targetElement.getChild(indices[i]); - } + Element editor = getSubPartElementEditor(args); + if (editor != null) { + return DOM.asOld(editor); } - return targetElement; + return null; } - private Element getCellFromRow(TableRowElement rowElement, int index) { - int childCount = rowElement.getCells().getLength(); - if (index < 0 || index >= childCount) { + private Element getSubPartElementEditor(SubPartArguments args) { + + if (!args.getType().equalsIgnoreCase("editor") + || editor.getState() != State.ACTIVE) { return null; } - TableCellElement currentCell = null; - boolean indexInColspan = false; - int i = 0; - - while (!indexInColspan) { - currentCell = rowElement.getCells().getItem(i); - - // Calculate if this is the cell we are looking for - int colSpan = currentCell.getColSpan(); - indexInColspan = index < colSpan + i; - - // Increment by colspan to skip over hidden cells - i += colSpan; + if (args.getIndicesLength() == 0) { + return editor.editorOverlay; + } else if (args.getIndicesLength() == 1 + && args.getIndex(0) < columns.size()) { + escalator + .scrollToColumn(args.getIndex(0), ScrollDestination.ANY, 0); + return editor.getWidget(columns.get(args.getIndex(0))).getElement(); } - return currentCell; + + return null; } @Override + @SuppressWarnings("deprecation") public String getSubPartName(com.google.gwt.user.client.Element subElement) { - // Containers and matching SubPart types - List<RowContainer> containers = Arrays.asList(escalator.getHeader(), - escalator.getBody(), escalator.getFooter()); - List<String> containerType = Arrays.asList("header", "cell", "footer"); - for (int i = 0; i < containers.size(); ++i) { - RowContainer container = containers.get(i); - boolean containerRow = (subElement.getTagName().equalsIgnoreCase( - "tr") && subElement.getParentElement() == container - .getElement()); - if (containerRow) { - // Wanted SubPart is row that is a child of containers root - // To get indices, we use a cell that is a child of this row - subElement = DOM.asOld(subElement.getFirstChildElement()); - } + String escalatorStructureName = escalator.getSubPartName(subElement); + if (escalatorStructureName != null) { + return escalatorStructureName.replaceFirst("^spacer", "details"); + } - Cell cell = container.getCell(subElement); - if (cell != null) { - // Skip the column index if subElement was a child of root - return containerType.get(i) + "[" + cell.getRow() - + (containerRow ? "]" : "][" + cell.getColumn() + "]"); - } + String editorName = getSubPartNameEditor(subElement); + if (editorName != null) { + return editorName; } - // Check if subelement is part of editor. - if (editor.getState() == State.ACTIVE) { - if (editor.editorOverlay.isOrHasChild(subElement)) { - int i = 0; - for (Column<?, T> column : columns) { - if (editor.getWidget(column).getElement() - .isOrHasChild(subElement)) { - return "editor[" + i + "]"; - } - ++i; - } - return "editor"; + return null; + } + + private String getSubPartNameEditor(Element subElement) { + + if (editor.getState() != State.ACTIVE + || !editor.editorOverlay.isOrHasChild(subElement)) { + return null; + } + + int i = 0; + for (Column<?, T> column : columns) { + if (editor.getWidget(column).getElement().isOrHasChild(subElement)) { + return "editor[" + i + "]"; } + ++i; } - return null; + return "editor"; } private void setSelectColumnRenderer( @@ -5913,6 +7079,34 @@ public class Grid<T> extends ResizeComposite implements } /** + * Register a column reorder handler to this Grid. The event for this + * handler is fired when the Grid's columns are reordered. + * + * @since 7.5.0 + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnReorderHandler( + ColumnReorderHandler<T> handler) { + return addHandler(handler, ColumnReorderEvent.getType()); + } + + /** + * Register a column visibility change handler to this Grid. The event for + * this handler is fired when the Grid's columns change visibility. + * + * @since 7.5.0 + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnVisibilityChangeHandler( + ColumnVisibilityChangeHandler<T> handler) { + return addHandler(handler, ColumnVisibilityChangeEvent.getType()); + } + + /** * Apply sorting to data source. */ private void sort(boolean userOriginated) { @@ -5964,6 +7158,27 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns whether columns can be reordered with drag and drop. + * + * @since 7.5.0 + * @return <code>true</code> if columns can be reordered, false otherwise + */ + public boolean isColumnReorderingAllowed() { + return columnReorderingAllowed; + } + + /** + * Sets whether column reordering with drag and drop is allowed or not. + * + * @since 7.5.0 + * @param columnReorderingAllowed + * specifies whether column reordering is allowed + */ + public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { + this.columnReorderingAllowed = columnReorderingAllowed; + } + + /** * Sets a new column order for the grid. All columns which are not ordered * here will remain in the order they were before as the last columns of * grid. @@ -5999,8 +7214,10 @@ public class Grid<T> extends ResizeComposite implements } columns = newOrder; + List<Column<?, T>> visibleColumns = getVisibleColumns(); + // Do ComplexRenderer.init and render new content - conf.insertColumns(0, columns.size()); + conf.insertColumns(0, visibleColumns.size()); // Number of frozen columns should be kept same #16901 updateFrozenColumns(); @@ -6017,6 +7234,10 @@ public class Grid<T> extends ResizeComposite implements for (FooterRow row : footer.getRows()) { row.calculateColspans(); } + + columnHider.updateTogglesOrder(); + + fireEvent(new ColumnReorderEvent<T>()); } /** @@ -6347,6 +7568,30 @@ public class Grid<T> extends ResizeComposite implements widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent); }-*/; + private static native final void onAttach(Widget widget) + /*-{ + widget.@Widget::onAttach()(); + }-*/; + + private static native final void onDetach(Widget widget) + /*-{ + widget.@Widget::onDetach()(); + }-*/; + + @Override + protected void doAttachChildren() { + if (getSidebar().getParent() == this) { + onAttach(getSidebar()); + } + } + + @Override + protected void doDetachChildren() { + if (getSidebar().getParent() == this) { + onDetach(getSidebar()); + } + } + /** * Resets all cached pixel sizes and reads new values from the DOM. This * methods should be used e.g. when styles affecting the dimensions of @@ -6357,6 +7602,100 @@ public class Grid<T> extends ResizeComposite implements } /** + * Sets a new details generator for row details. + * <p> + * The currently opened row details will be re-rendered. + * + * @since 7.5.0 + * @param detailsGenerator + * the details generator to set + * @throws IllegalArgumentException + * if detailsGenerator is <code>null</code>; + */ + public void setDetailsGenerator(DetailsGenerator detailsGenerator) + throws IllegalArgumentException { + + if (detailsGenerator == null) { + throw new IllegalArgumentException( + "Details generator may not be null"); + } + + this.detailsGenerator = detailsGenerator; + + // this will refresh all visible spacers + escalator.getBody().setSpacerUpdater(gridSpacerUpdater); + } + + /** + * Gets the current details generator for row details. + * + * @since 7.5.0 + * @return the detailsGenerator the current details generator + */ + public DetailsGenerator getDetailsGenerator() { + return detailsGenerator; + } + + /** + * Shows or hides the details for a specific row. + * <p> + * This method does nothing if trying to set show already-visible details, + * or hide already-hidden details. + * + * @since 7.5.0 + * @param rowIndex + * the index of the affected row + * @param visible + * <code>true</code> to show the details, or <code>false</code> + * to hide them + * @see #isDetailsVisible(int) + */ + public void setDetailsVisible(int rowIndex, boolean visible) { + Integer rowIndexInteger = Integer.valueOf(rowIndex); + + /* + * We want to prevent opening a details row twice, so any subsequent + * openings (or closings) of details is a NOOP. + * + * When a details row is opened, it is given an arbitrary height + * (because Escalator requires a height upon opening). Only when it's + * opened, Escalator will ask the generator to generate a widget, which + * we then can measure. When measured, we correct the initial height by + * the original height. + * + * Without this check, we would override the measured height, and revert + * back to the initial, arbitrary, height which would most probably be + * wrong. + * + * see GridSpacerUpdater.init for implementation details. + */ + + boolean isVisible = isDetailsVisible(rowIndex); + if (visible && !isVisible) { + escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT); + visibleDetails.add(rowIndexInteger); + } + + else if (!visible && isVisible) { + escalator.getBody().setSpacer(rowIndex, -1); + visibleDetails.remove(rowIndexInteger); + } + } + + /** + * Check whether the details for a row is visible or not. + * + * @since 7.5.0 + * @param rowIndex + * the index of the row for which to check details + * @return <code>true</code> iff the details for the given row is visible + * @see #setDetailsVisible(int, boolean) + */ + public boolean isDetailsVisible(int rowIndex) { + return visibleDetails.contains(Integer.valueOf(rowIndex)); + } + + /** * Requests that the column widths should be recalculated. * <p> * The actual recalculation is not necessarily done immediately so you @@ -6368,4 +7707,63 @@ public class Grid<T> extends ResizeComposite implements public void recalculateColumnWidths() { autoColumnWidthsRecalculator.schedule(); } + + /** + * Returns the sidebar for this grid. + * <p> + * The grid's sidebar shows the column hiding options for those columns that + * have been set as {@link Column#setHidable(boolean) hidable}. + * + * @since 7.5.0 + * @return the sidebar widget for this grid + */ + private Sidebar getSidebar() { + return sidebar; + } + + /** + * 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 + * the items in the beginning of the menu that control the column hiding if + * any columns are marked as hidable. A toggle for opening the menu will be + * displayed whenever the menu contains at least one item. + * + * @since 7.5.0 + * @return the menu bar + */ + public MenuBar getSidebarMenu() { + return sidebar.menuBar; + } + + /** + * Tests whether the sidebar menu is currently open. + * + * @since 7.5.0 + * @see #getSidebarMenu() + * @return <code>true</code> if the sidebar is open; <code>false</code> if + * it is closed + */ + public boolean isSidebarOpen() { + return sidebar.isOpen(); + } + + /** + * Sets whether the sidebar menu is open. + * + * + * @since 7.5.0 + * @see #getSidebarMenu() + * @see #isSidebarOpen() + * @param sidebarOpen + * <code>true</code> to open the sidebar; <code>false</code> to + * close it + */ + public void setSidebarOpen(boolean sidebarOpen) { + if (sidebarOpen) { + sidebar.open(); + } else { + sidebar.close(); + } + } } |