From 7c6f4ef6e12836508f63cdee79c11aecdfa93291 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Thu, 12 Feb 2015 11:39:44 +0200 Subject: Drag and drop column reorder for default header row on client side Grid. #16643 Change-Id: I94558915859aaa91bdd041da12094bc0dcc53e62 --- .../vaadin/client/ui/dd/DragAndDropHandler.java | 240 +++++++++++++++++++++ client/src/com/vaadin/client/widgets/Grid.java | 209 +++++++++++++++++- 2 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java (limited to 'client') 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..c8b651bd4e --- /dev/null +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -0,0 +1,240 @@ +/* + * 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 java.util.logging.Logger; + +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. Loosely + * based on {@link VDragAndDropManager}, but without any Vaadin related stuff. + * Properly handles drag start, cancel and end. For example, used in + * {@link Grid} column header reordering. + *

+ * 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. + */ + void showDragElement(); + + /** + * Called on drag. + * + * @param event + * the event related to the drag + */ + void updateDragElement(NativePreviewEvent event); + + /** + * Called when the has ended on a drop or cancel. + */ + void removeDragElement(); + + /** + * Called when the drag has ended on a drop. + */ + void onDrop(); + + /** + * Called when the drag has been canceled before drop. + */ + 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.updateDragElement(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.updateDragElement(event); + callback.onDrop(); + stopDrag(); + event.cancel(); + break; + default: + break; + } + } else { + stopDrag(); + } + } + + private void cancelDrag(NativePreviewEvent event) { + callback.onDragCancel(); + callback.removeDragElement(); + stopDrag(); + event.cancel(); + event.getNativeEvent().preventDefault(); + } + + }; + + private static Logger getLogger() { + return Logger.getLogger(DragAndDropHandler.class.getName()); + } + + /** + * 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(event, callback); + } + break; + default: + // on any other events, clean up this preview + // listener + removeNativePreviewHandlerRegistration(); + break; + } + } + }); + } + + private void startDrag(NativePreviewEvent event, + DragAndDropCallback callback) { + dragging = true; + // just capture something to prevent text selection in IE + Event.setCapture(RootPanel.getBodyElement()); + this.callback = callback; + dragHandlerRegistration = Event.addNativePreviewHandler(dragHandler); + callback.showDragElement(); + callback.updateDragElement(event); + } + + private void stopDrag() { + dragging = false; + if (dragHandlerRegistration != null) { + dragHandlerRegistration.removeHandler(); + dragHandlerRegistration = null; + } + Event.releaseCapture(RootPanel.getBodyElement()); + if (callback != null) { + callback.removeDragElement(); + callback = null; + } + } + + private void removeNativePreviewHandlerRegistration() { + if (dragStartNativePreviewHandlerRegistration != null) { + dragStartNativePreviewHandlerRegistration.removeHandler(); + dragStartNativePreviewHandlerRegistration = null; + } + } +} diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 71962d6953..b0e18cc2ab 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -36,6 +36,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.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; @@ -53,6 +55,8 @@ 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; @@ -68,6 +72,8 @@ 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.ui.dd.DragAndDropHandler.DragAndDropCallback; import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; @@ -2853,6 +2859,153 @@ public class Grid extends ResizeComposite implements private boolean enabled = true; + private boolean columnReorderingAllowed; + + private DragAndDropHandler dndHandler = new DragAndDropHandler(); + + private DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() { + + /** + * 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; + /** + * Makes sure that drag cancel doesn't cause anything unwanted like sort + */ + private HandlerRegistration columnSortPreventRegistration; + + 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"); + getElement().appendChild(table); + } + + @Override + public void updateDragElement(NativePreviewEvent event) { + int clientX = WidgetUtil.getTouchOrMouseClientX(event + .getNativeEvent()); + resolveDragElementHorizontalPosition(clientX); + updateDragDropMarker(clientX); + } + + private void updateDragDropMarker(final int clientX) { + RowContainer header = escalator.getHeader(); + NodeList cells = header.getRowElement( + eventCell.getRowIndex()).getCells(); + double dropMarkerLeft = 0 - escalator.getScrollLeft(); + latestColumnDropIndex = 0; + for (int i = 0; i < cells.getLength(); i++, latestColumnDropIndex++) { + TableCellElement cellElement = cells.getItem(i); + int cellX = cellElement.getAbsoluteLeft(); + int cellWidth = cellElement.getOffsetWidth(); + if (clientX < cellX || clientX < cellX + (cellWidth / 2)) { + break; + } else { + dropMarkerLeft += cellWidth; + } + } + if (dropMarkerLeft > header.getElement().getOffsetWidth() + || dropMarkerLeft < 0) { + dropMarkerLeft = -10000000; + } + dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); + } + + private void resolveDragElementVerticalPosition() { + dragElement.getStyle().setTop(-10, Unit.PX); + } + + private void resolveDragElementHorizontalPosition(final int clientX) { + int left = clientX - table.getAbsoluteLeft(); + left = Math.max(0, Math.min(left, table.getClientWidth())); + left -= dragElement.getClientWidth() / 2; + dragElement.getStyle().setLeft(left, Unit.PX); + } + + @Override + public void showDragElement() { + initHeaderDragElementDOM(); + // TODO this clones also some unwanted style names, should confirm + // with UX what we want to show (focus/sort indicator) + dragElement = DOM.clone(eventCell.getElement(), true); + dragElement.getStyle().clearWidth(); + dropMarker.getStyle().setProperty("height", + dragElement.getStyle().getHeight()); + tableHeader.appendChild(dragElement); + // might need to change this on fly once sorting with multiple + // header rows is possible + resolveDragElementVerticalPosition(); + } + + @Override + public void removeDragElement() { + table.removeFromParent(); + dragElement.removeFromParent(); + } + + @Override + public void onDrop() { + final int draggedColumnIndex = eventCell.getColumnIndex(); + if (latestColumnDropIndex != draggedColumnIndex + && latestColumnDropIndex != (draggedColumnIndex + 1)) { + List> columns = getColumns(); + List> reordered = new ArrayList>( + columns); + Column moved = reordered.remove(draggedColumnIndex); + if (draggedColumnIndex < latestColumnDropIndex) { + latestColumnDropIndex--; + } + reordered.add(latestColumnDropIndex, moved); + @SuppressWarnings("unchecked") + Column[] array = reordered.toArray(new Column[reordered + .size()]); + setColumnOrder(array); + } // else no reordering + } + + @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; + } + } + }); + } + } + }; + /** * Enumeration for easy setting of selection mode. */ @@ -4649,7 +4802,7 @@ public class Grid extends ResizeComposite implements + getColumnCount() + ")"); } - this.frozenColumnCount = numberOfColumns; + frozenColumnCount = numberOfColumns; updateFrozenColumns(); } @@ -4949,6 +5102,10 @@ public class Grid extends ResizeComposite implements if (!isElementInChildWidget(e)) { + if (handleHeaderCellDragStartEvent(event, container)) { + return; + } + // Sorting through header Click / KeyUp if (handleHeaderDefaultRowEvent(event, container)) { return; @@ -5097,6 +5254,35 @@ public class Grid extends ResizeComposite implements return true; } + private boolean handleHeaderCellDragStartEvent(Event event, + RowContainer container) { + if (!columnReorderingAllowed) { + return false; + } + if (container != escalator.getHeader()) { + return false; + } + // for now only support reordering of default row as the only row + if (!getHeader().getRow(eventCell.getRowIndex()).isDefault() + || getHeader().getRowCount() != 1) { + 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 cellStyleGenerator; private RowStyleGenerator rowStyleGenerator; @@ -5905,6 +6091,27 @@ public class Grid extends ResizeComposite implements || autoColumnWidthsRecalculator.isScheduled(); } + /** + * Returns whether columns can be reordered with drag and drop. + * + * @since + * @return true 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 + * @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 -- cgit v1.2.3 From 556c1aa06c547933bc36f347693c4b5b85bac149 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Wed, 11 Feb 2015 14:52:02 +0200 Subject: Escalator supports adding spacer elements into DOM. (#16644) This is the first step towards Grid's details rows: Escalator puts spacer elements in the DOM, and is able to scroll around with them. The spacers are put in their correct locations, but they will not affect the normal row elements in any way at this time. Change-Id: Id20090c4de117e07e332dcc81e9964360f778258 --- .../VAADIN/themes/base/escalator/escalator.scss | 13 + .../client/widget/escalator/RowContainer.java | 39 +- .../src/com/vaadin/client/widgets/Escalator.java | 451 ++++++++++++--------- .../EscalatorBasicClientFeaturesTest.java | 26 ++ .../escalator/EscalatorSpacerTest.java | 48 +++ .../grid/EscalatorBasicClientFeaturesWidget.java | 29 ++ .../widgetset/client/grid/EscalatorProxy.java | 23 +- 7 files changed, 441 insertions(+), 188 deletions(-) create mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java (limited to 'client') diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss index 606dc6a7dd..6d146f3a74 100644 --- a/WebContent/VAADIN/themes/base/escalator/escalator.scss +++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss @@ -133,4 +133,17 @@ z-index: 1; } + .#{$primaryStyleName}-spacer { + position: absolute; + display: block; + + // debug + background-color: rgba(0,0,0,0.6); + color: white; + + > td { + width: 100%; + height: 100%; + } + } } diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java index 397336450e..5cc58cb47e 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -22,16 +22,47 @@ 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}. + *

+ * The body section can contain both rows and spacers. + * + * @since + * @author Vaadin Ltd + * @see com.vaadin.client.widgets.Escalator#getBody() + */ + public interface BodyRowContainer extends RowContainer { + /** + * Marks a spacer and its height. + *

+ * If a spacer is already registered with the given row index, that + * spacer will be updated with the given height. + * + * @param rowIndex + * the row index for the spacer to modify. The affected + * spacer is underneath the given index + * @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 + */ + void setSpacer(int rowIndex, double height) + throws 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/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index ca54b97ca5..2536356c94 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -23,6 +23,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; @@ -67,6 +68,7 @@ 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; @@ -93,7 +95,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 @@ -106,7 +108,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 @@ -118,8 +120,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 @@ -137,9 +139,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 @@ -147,20 +149,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 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. */ @@ -271,12 +273,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * that it _might_ perform better (rememeber to measure, implement, * re-measure) */ - /* - * [[rowheight]]: This code will require alterations that are relevant for - * being able to support variable row heights. NOTE: these bits can most - * often also be identified by searching for code reading the ROW_HEIGHT_PX - * constant. - */ /* * [[mpixscroll]]: This code will require alterations that are relevant for * supporting the scrolling through more pixels than some browsers normally @@ -284,6 +280,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 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 @@ -1139,10 +1138,6 @@ 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 targetEndPx = targetStartPx + body.getDefaultRowHeight(); @@ -1183,19 +1178,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ private String primaryStyleName = null; - /** - * A map containing cached values of an element's current top position. - *

- * 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 rowTopPositionMap = new HashMap(); - private boolean defaultRowHeightShouldBeAutodetected = true; private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; @@ -1907,20 +1889,17 @@ 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") 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() { @@ -2204,18 +2183,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); @@ -2290,7 +2258,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } - private class BodyRowContainer extends AbstractRowContainer { + private class BodyRowContainerImpl extends AbstractRowContainer implements + BodyRowContainer { /* * TODO [[optimize]]: check whether a native JsArray might be faster * than LinkedList @@ -2401,7 +2370,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); } @@ -2409,6 +2380,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() { @@ -2431,21 +2403,13 @@ 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 / 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()); moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); @@ -2456,11 +2420,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } else if (viewportOffset + getDefaultRowHeight() <= 0) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - /* * the viewport has been scrolled more than the topmost visual * row. @@ -2469,10 +2428,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker int originalRowsToMove = (int) Math.abs(viewportOffset / 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,10 +2439,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker logicalRowIndex = getLogicalRowIndex(visualRowOrder .getLast()) + 1; } else { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ /* * Since we're moving all escalator rows, we need to * calculate the first logical row index from the scroll @@ -2498,13 +2453,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 @@ -2583,10 +2538,6 @@ 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 @@ -2600,10 +2551,6 @@ 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); updateTopRowLogicalIndex(numberOfRows); @@ -2637,10 +2584,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker 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(); @@ -2649,10 +2592,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker 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(); } @@ -2762,10 +2701,6 @@ 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(); final ListIterator iter = visualRowOrder @@ -2773,10 +2708,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker 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(); } } @@ -2802,10 +2733,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker verticalScrollbar.setScrollPosByDelta(yDelta); - /* - * 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); @@ -2847,10 +2774,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 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()); } @@ -2859,10 +2782,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker 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()); } @@ -2873,10 +2792,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } 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; @@ -2930,10 +2845,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .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(); @@ -2996,10 +2907,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final int dirtyRowsStart = removedLogicalInside.getStart(); for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ setRowPosition(tr, 0, i * getDefaultRowHeight()); } @@ -3039,10 +2946,6 @@ 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 @@ -3097,7 +3000,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 */ @@ -3160,12 +3063,6 @@ 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(); } @@ -3214,10 +3111,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())); @@ -3269,20 +3162,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final ListIterator 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(); 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(); } } @@ -3305,19 +3190,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker // move the surrounding rows to their correct places. final ListIterator 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(); 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(); } } @@ -3364,8 +3241,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 @@ -3586,10 +3463,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) { @@ -3614,20 +3487,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 */ @@ -3661,10 +3520,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); @@ -3775,6 +3630,12 @@ 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); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4250,6 +4111,226 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + private class SpacerContainer { + + /** This is used mainly for testing purposes */ + private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; + + /* + * TODO [[optimize]] maybe convert the usage of this class to flyweight + * or object pooling pattern? + */ + private final class Spacer { + private TableCellElement spacerElement; + private TableRowElement root; + + public TableRowElement getRootElement() { + return root; + } + + public void setPosition(double x, double y) { + positions.set(root, x, y); + } + + /** + * Creates a new element structure for the spacer. + *

+ * {@link #createDomStructure()} and + * {@link #setRootElement(Element)} can collectively only be called + * once, otherwise an {@link AssertionError} will be raised (if + * asserts are enabled). + */ + public void createDomStructure(double height) { + assert root == null || root.getParentElement() == null : "this spacer was already attached"; + + root = TableRowElement.as(DOM.createTR()); + spacerElement = TableCellElement.as(DOM.createTD()); + root.appendChild(spacerElement); + spacerElement.setInnerText("IAMA SPACER, AMA"); + initElements(height); + } + + private void initElements(double height) { + setHeight(height); + root.getStyle().setWidth(100, Unit.PCT); + + spacerElement.getStyle().setWidth(100, Unit.PCT); + spacerElement.setColSpan(getColumnConfiguration() + .getColumnCount()); + + setStylePrimaryName(getStylePrimaryName()); + } + + public void setRootElement(TableRowElement tr) { + assert root == null || root.getParentElement() == null : "this spacer was already attached"; + + assert tr != null : "tr may not be null"; + root = tr; + + assert tr.getChildCount() == 1 : "tr must have exactly one child"; + spacerElement = tr.getCells().getItem(0); + assert spacerElement != null : "spacer element somehow was null"; + } + + public void setStylePrimaryName(String style) { + UIObject.setStylePrimaryName(root, style + "-spacer"); + } + + public void setHeight(double newHeight) { + root.getStyle().setHeight(newHeight, Unit.PX); + getLogger().warning( + "spacer's height changed, but pushing rows out of " + + "the way not implemented yet"); + } + } + + private final TreeMap rowIndexToHeight = new TreeMap(); + private final TreeMap rowIndexToSpacerElement = new TreeMap(); + + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + if (rowIndex < 0 || rowIndex >= getBody().getRowCount()) { + throw new IllegalArgumentException("invalid row index: " + + rowIndex + ", while the body only has " + + getBody().getRowCount() + " rows."); + } + + if (height >= 0) { + insertOrUpdateSpacer(rowIndex, height); + } else if (spacerExists(rowIndex)) { + removeSpacer(rowIndex); + } + } + + @SuppressWarnings("boxing") + private void insertOrUpdateSpacer(int rowIndex, double height) { + if (!spacerExists(rowIndex)) { + insertSpacer(rowIndex, height); + } else { + updateSpacer(rowIndex, height); + } + rowIndexToHeight.put(rowIndex, height); + } + + private boolean spacerExists(int rowIndex) { + Integer rowIndexObj = Integer.valueOf(rowIndex); + boolean spacerExists = rowIndexToHeight.containsKey(rowIndexObj); + assert spacerExists == rowIndexToSpacerElement + .containsKey(rowIndexObj) : "Inconsistent bookkeeping detected."; + return spacerExists; + } + + @SuppressWarnings("boxing") + private void insertSpacer(int rowIndex, double height) { + Spacer spacer = createSpacer(height); + spacer.getRootElement().setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, + rowIndex); + TableRowElement spacerRoot = spacer.getRootElement(); + rowIndexToSpacerElement.put(rowIndex, spacerRoot); + spacer.setPosition(0, getSpacerTop(rowIndex)); + spacerRoot.getStyle().setWidth( + columnConfiguration.calculateRowWidth(), Unit.PX); + body.getElement().appendChild(spacerRoot); + } + + private void updateSpacer(int rowIndex, double newHeight) { + getSpacer(rowIndex).setHeight(newHeight); + } + + @SuppressWarnings("boxing") + private Spacer getSpacer(int rowIndex) { + Spacer spacer = new Spacer(); + spacer.setRootElement(rowIndexToSpacerElement.get(rowIndex)); + return spacer; + } + + private Spacer createSpacer(double height) { + /* + * Optimally, this would be a factory method in SpacerImpl, but + * since it's not a static class, we can't do that directly. We + * could make it static, and pass in references, but that probably + * will become hairy pretty quickly. + */ + + Spacer spacer = new Spacer(); + spacer.createDomStructure(height); + return spacer; + } + + private double getSpacerTop(int rowIndex) { + double spacerHeights = 0; + + /*- + * TODO: uncomment this bit once the spacers start pushing the + * rows downwards, offseting indices. OTOH this entire method + * probably needs to be usable by BodyContainerImpl as well, + * since this same logic can/should be used for calculating the + * top position for rows. + + // Sum all spacer heights that occur before rowIndex. + for (Double spacerHeight : rowIndexToHeight.headMap( + Integer.valueOf(rowIndex), false).values()) { + spacerHeights += spacerHeight.doubleValue(); + } + */ + + double rowHeights = getBody().getDefaultRowHeight() + * (rowIndex + 1); + + return rowHeights + spacerHeights; + } + + @SuppressWarnings("boxing") + private void removeSpacer(int rowIndex) { + Spacer spacer = getSpacer(rowIndex); + + // fix DOM + spacer.setHeight(0); // resets row offsets + spacer.getRootElement().removeFromParent(); + + // fix bookkeeping + rowIndexToHeight.remove(rowIndex); + rowIndexToSpacerElement.remove(rowIndex); + } + + public void setStylePrimaryName(String style) { + for (TableRowElement spacerRoot : rowIndexToSpacerElement.values()) { + Spacer spacer = new Spacer(); + spacer.setRootElement(spacerRoot); + spacer.setStylePrimaryName(style); + } + } + } + + private class ElementPositionBookkeeper { + /** + * A map containing cached values of an element's current top position. + *

+ * Don't use this field directly, because it will not take proper care + * of all the bookkeeping required. + */ + private final Map elementTopPositionMap = new HashMap(); + + 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)); + } + + 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 void remove(Element e) { + elementTopPositionMap.remove(e); + } + } + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y /** * The solution to @@ -4309,7 +4390,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(); @@ -4346,6 +4427,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } }; + private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper(); + /** * Creates a new Escalator widget instance. */ @@ -4517,7 +4600,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker 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); @@ -4595,7 +4678,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * * @return the body. Never null */ - public RowContainer getBody() { + public BodyRowContainer getBody() { return body; } @@ -5190,4 +5273,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker columnConfiguration.getColumnWidth(i)); } } + + private Range getViewportPixels() { + int from = (int) Math.floor(verticalScrollbar.getScrollPos()); + int to = (int) Math.ceil(body.heightOfSection); + return Range.between(from, to); + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index 92c7f3e6a6..b8bc644948 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.List; + import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; @@ -64,6 +66,10 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String COLUMN_SPANNING = "Column spanning"; protected static final String COLSPAN_NORMAL = "Apply normal colspan"; protected static final String COLSPAN_NONE = "Apply no colspan"; + protected static final String SPACERS = "Spacers"; + protected static final String ROW_1 = "Row 1"; + protected static final String SET_100PX = "Set 100px"; + protected static final String REMOVE = "Remove"; @Override protected Class getUIClass() { @@ -259,4 +265,24 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected void populate() { selectMenuPath(GENERAL, POPULATE_COLUMN_ROW); } + + private List getSpacers() { + return getEscalator().findElements(By.className("v-escalator-spacer")); + } + + @SuppressWarnings("boxing") + protected WebElement getSpacer(int logicalRowIndex) { + List spacers = getSpacers(); + System.out.println("size: " + spacers.size()); + for (WebElement spacer : spacers) { + System.out.println(spacer + ", " + logicalRowIndex); + Boolean isInDom = (Boolean) executeScript( + "return arguments[0]['vLogicalRow'] === arguments[1]", + spacer, logicalRowIndex); + if (isInDom) { + return spacer; + } + } + return null; + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java new file mode 100644 index 0000000000..24e474a767 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.escalator; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; + +public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { + + @Before + public void before() { + openTestURL(); + populate(); + } + + @Test + public void openVisibleSpacer() { + assertNull("No spacers should be shown at the start", getSpacer(1)); + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + assertNotNull("Spacer should be shown after setting it", getSpacer(1)); + } + + @Test + public void closeVisibleSpacer() { + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + selectMenuPath(FEATURES, SPACERS, ROW_1, REMOVE); + assertNull("Spacer should not exist after removing it", getSpacer(1)); + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index 761f32bc9a..0905cde4eb 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -303,6 +303,7 @@ public class EscalatorBasicClientFeaturesWidget extends createColumnsAndRowsMenu(); createFrozenMenu(); createColspanMenu(); + createSpacerMenu(); } private void createFrozenMenu() { @@ -612,6 +613,34 @@ public class EscalatorBasicClientFeaturesWidget extends }, menupath); } + private void createSpacerMenu() { + String[] menupath = { "Features", "Spacers" }; + createSpacersMenuForRow(1, menupath); + createSpacersMenuForRow(50, menupath); + } + + private void createSpacersMenuForRow(final int rowIndex, String[] menupath) { + menupath = new String[] { menupath[0], menupath[1], "Row " + rowIndex }; + addMenuCommand("Set 100px", new ScheduledCommand() { + @Override + public void execute() { + escalator.getBody().setSpacer(rowIndex, 100); + } + }, menupath); + addMenuCommand("Set 50px", new ScheduledCommand() { + @Override + public void execute() { + escalator.getBody().setSpacer(rowIndex, 50); + } + }, menupath); + addMenuCommand("Remove", new ScheduledCommand() { + @Override + public void execute() { + escalator.getBody().setSpacer(rowIndex, -1); + } + }, menupath); + } + private void insertRows(final RowContainer container, int offset, int number) { if (container == escalator.getBody()) { data.insertRows(offset, number); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java index 7f813b9d0f..63286af86f 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java @@ -24,6 +24,7 @@ import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; import com.vaadin.client.widget.escalator.RowContainer; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widgets.Escalator; import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget; @@ -97,6 +98,22 @@ public class EscalatorProxy extends Escalator { } } + private class BodyRowContainerProxy extends RowContainerProxy implements + BodyRowContainer { + private BodyRowContainer rowContainer; + + public BodyRowContainerProxy(BodyRowContainer rowContainer) { + super(rowContainer); + this.rowContainer = rowContainer; + } + + @Override + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + rowContainer.setSpacer(rowIndex, height); + } + } + private class RowContainerProxy implements RowContainer { private final RowContainer rowContainer; @@ -176,7 +193,7 @@ public class EscalatorProxy extends Escalator { } private RowContainer headerProxy = null; - private RowContainer bodyProxy = null; + private BodyRowContainer bodyProxy = null; private RowContainer footerProxy = null; private ColumnConfiguration columnProxy = null; private LogWidget logWidget; @@ -198,9 +215,9 @@ public class EscalatorProxy extends Escalator { } @Override - public RowContainer getBody() { + public BodyRowContainer getBody() { if (bodyProxy == null) { - bodyProxy = new RowContainerProxy(super.getBody()); + bodyProxy = new BodyRowContainerProxy(super.getBody()); } return bodyProxy; } -- cgit v1.2.3 From c72967ba77c2bc0fcc56df499f5a75f6c7ca2c07 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Thu, 12 Feb 2015 15:04:31 +0200 Subject: Adds SpacerUpdater support for Escalator (#16644) Change-Id: I9d73a3fc36e94e36c35859a4ae456002c75df2f5 --- .../client/widget/escalator/EscalatorUpdater.java | 2 - .../com/vaadin/client/widget/escalator/Row.java | 1 - .../client/widget/escalator/RowContainer.java | 27 +++ .../com/vaadin/client/widget/escalator/Spacer.java | 41 +++++ .../client/widget/escalator/SpacerUpdater.java | 62 +++++++ .../src/com/vaadin/client/widgets/Escalator.java | 190 +++++++++++---------- .../grid/EscalatorBasicClientFeaturesWidget.java | 30 ++++ .../widgetset/client/grid/EscalatorProxy.java | 12 ++ 8 files changed, 275 insertions(+), 90 deletions(-) create mode 100644 client/src/com/vaadin/client/widget/escalator/Spacer.java create mode 100644 client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java (limited to 'client') 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 5cc58cb47e..ea56c25062 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -44,6 +44,7 @@ public interface RowContainer { * @see com.vaadin.client.widgets.Escalator#getBody() */ public interface BodyRowContainer extends RowContainer { + /** * Marks a spacer and its height. *

@@ -61,6 +62,32 @@ public interface RowContainer { */ void setSpacer(int rowIndex, double height) throws IllegalArgumentException; + + /** + * Sets a new spacer updater. + *

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

+ * {@link SpacerUpdater#NULL} is the default. + * + * @return the spacer updater currently in use. Never null + */ + SpacerUpdater getSpacerUpdater(); + } /** 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..0b0a2b257f --- /dev/null +++ b/client/src/com/vaadin/client/widget/escalator/Spacer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.escalator; + +import com.google.gwt.dom.client.Element; + +/** + * A representation of a spacer element in a {@link SpacerContainer}. + * + * @since + * @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 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..18f53db507 --- /dev/null +++ b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.escalator; + +/** + * An interface that handles the display of content for spacers. + *

+ * The updater is responsible for making sure all elements are properly + * constructed and cleaned up. + * + * @since + * @author Vaadin Ltd + * @see Spacer + * @see SpacerContainer + */ +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. + *

+ * 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/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 2536356c94..6c6998277f 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -74,6 +74,8 @@ 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; @@ -3636,6 +3638,17 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker throws IllegalArgumentException { spacerContainer.setSpacer(rowIndex, height); } + + @Override + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + spacerContainer.setSpacerUpdater(spacerUpdater); + } + + @Override + public SpacerUpdater getSpacerUpdater() { + return spacerContainer.getSpacerUpdater(); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4116,41 +4129,21 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker /** This is used mainly for testing purposes */ private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; - /* - * TODO [[optimize]] maybe convert the usage of this class to flyweight - * or object pooling pattern? - */ - private final class Spacer { + private final class SpacerImpl implements Spacer { private TableCellElement spacerElement; private TableRowElement root; + private final int rowIndex; - public TableRowElement getRootElement() { - return root; - } - - public void setPosition(double x, double y) { - positions.set(root, x, y); - } - - /** - * Creates a new element structure for the spacer. - *

- * {@link #createDomStructure()} and - * {@link #setRootElement(Element)} can collectively only be called - * once, otherwise an {@link AssertionError} will be raised (if - * asserts are enabled). - */ - public void createDomStructure(double height) { - assert root == null || root.getParentElement() == null : "this spacer was already attached"; + public SpacerImpl(int rowIndex, double height) { + this.rowIndex = rowIndex; + // Build DOM structure root = TableRowElement.as(DOM.createTR()); spacerElement = TableCellElement.as(DOM.createTD()); root.appendChild(spacerElement); - spacerElement.setInnerText("IAMA SPACER, AMA"); - initElements(height); - } + root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); - private void initElements(double height) { + // Configure DOM structure setHeight(height); root.getStyle().setWidth(100, Unit.PCT); @@ -4161,15 +4154,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker setStylePrimaryName(getStylePrimaryName()); } - public void setRootElement(TableRowElement tr) { - assert root == null || root.getParentElement() == null : "this spacer was already attached"; - - assert tr != null : "tr may not be null"; - root = tr; + public TableRowElement getRootElement() { + return root; + } - assert tr.getChildCount() == 1 : "tr must have exactly one child"; - spacerElement = tr.getCells().getItem(0); - assert spacerElement != null : "spacer element somehow was null"; + public void setPosition(double x, double y) { + positions.set(root, x, y); } public void setStylePrimaryName(String style) { @@ -4182,10 +4172,20 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker "spacer's height changed, but pushing rows out of " + "the way not implemented yet"); } + + @Override + public Element getElement() { + return spacerElement; + } + + @Override + public int getRow() { + return rowIndex; + } } - private final TreeMap rowIndexToHeight = new TreeMap(); - private final TreeMap rowIndexToSpacerElement = new TreeMap(); + private final TreeMap rowIndexToSpacer = new TreeMap(); + private SpacerUpdater spacerUpdater = SpacerUpdater.NULL; public void setSpacer(int rowIndex, double height) throws IllegalArgumentException { @@ -4196,65 +4196,42 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } if (height >= 0) { - insertOrUpdateSpacer(rowIndex, height); + if (!spacerExists(rowIndex)) { + insertNewSpacer(rowIndex, height); + } else { + updateExistingSpacer(rowIndex, height); + } } else if (spacerExists(rowIndex)) { removeSpacer(rowIndex); } } - @SuppressWarnings("boxing") - private void insertOrUpdateSpacer(int rowIndex, double height) { - if (!spacerExists(rowIndex)) { - insertSpacer(rowIndex, height); - } else { - updateSpacer(rowIndex, height); - } - rowIndexToHeight.put(rowIndex, height); - } - private boolean spacerExists(int rowIndex) { - Integer rowIndexObj = Integer.valueOf(rowIndex); - boolean spacerExists = rowIndexToHeight.containsKey(rowIndexObj); - assert spacerExists == rowIndexToSpacerElement - .containsKey(rowIndexObj) : "Inconsistent bookkeeping detected."; - return spacerExists; + return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex)); } @SuppressWarnings("boxing") - private void insertSpacer(int rowIndex, double height) { - Spacer spacer = createSpacer(height); - spacer.getRootElement().setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, - rowIndex); - TableRowElement spacerRoot = spacer.getRootElement(); - rowIndexToSpacerElement.put(rowIndex, spacerRoot); + private void insertNewSpacer(int rowIndex, double height) { + + SpacerImpl spacer = new SpacerImpl(rowIndex, height); + + rowIndexToSpacer.put(rowIndex, spacer); spacer.setPosition(0, getSpacerTop(rowIndex)); + + TableRowElement spacerRoot = spacer.getRootElement(); spacerRoot.getStyle().setWidth( columnConfiguration.calculateRowWidth(), Unit.PX); body.getElement().appendChild(spacerRoot); + + initSpacerContent(spacer); } - private void updateSpacer(int rowIndex, double newHeight) { + private void updateExistingSpacer(int rowIndex, double newHeight) { getSpacer(rowIndex).setHeight(newHeight); } - @SuppressWarnings("boxing") - private Spacer getSpacer(int rowIndex) { - Spacer spacer = new Spacer(); - spacer.setRootElement(rowIndexToSpacerElement.get(rowIndex)); - return spacer; - } - - private Spacer createSpacer(double height) { - /* - * Optimally, this would be a factory method in SpacerImpl, but - * since it's not a static class, we can't do that directly. We - * could make it static, and pass in references, but that probably - * will become hairy pretty quickly. - */ - - Spacer spacer = new Spacer(); - spacer.createDomStructure(height); - return spacer; + private SpacerImpl getSpacer(int rowIndex) { + return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); } private double getSpacerTop(int rowIndex) { @@ -4282,32 +4259,71 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker @SuppressWarnings("boxing") private void removeSpacer(int rowIndex) { - Spacer spacer = getSpacer(rowIndex); + SpacerImpl spacer = getSpacer(rowIndex); // fix DOM + destroySpacerContent(spacer); spacer.setHeight(0); // resets row offsets spacer.getRootElement().removeFromParent(); // fix bookkeeping - rowIndexToHeight.remove(rowIndex); - rowIndexToSpacerElement.remove(rowIndex); + rowIndexToSpacer.remove(rowIndex); } public void setStylePrimaryName(String style) { - for (TableRowElement spacerRoot : rowIndexToSpacerElement.values()) { - Spacer spacer = new Spacer(); - spacer.setRootElement(spacerRoot); + 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 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 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"; + } } private class ElementPositionBookkeeper { /** * A map containing cached values of an element's current top position. - *

- * Don't use this field directly, because it will not take proper care - * of all the bookkeeping required. */ private final Map elementTopPositionMap = new HashMap(); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index 0905cde4eb..37c0865e35 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -12,6 +12,9 @@ import com.vaadin.client.widget.escalator.EscalatorUpdater; import com.vaadin.client.widget.escalator.FlyweightCell; import com.vaadin.client.widget.escalator.Row; import com.vaadin.client.widget.escalator.RowContainer; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; +import com.vaadin.client.widget.escalator.Spacer; +import com.vaadin.client.widget.escalator.SpacerUpdater; import com.vaadin.client.widgets.Escalator; public class EscalatorBasicClientFeaturesWidget extends @@ -615,6 +618,33 @@ public class EscalatorBasicClientFeaturesWidget extends private void createSpacerMenu() { String[] menupath = { "Features", "Spacers" }; + + addMenuCommand("Swap Spacer Updater", new ScheduledCommand() { + private final SpacerUpdater CUSTOM = new SpacerUpdater() { + @Override + public void destroy(Spacer spacer) { + spacer.getElement().setInnerText(""); + } + + @Override + public void init(Spacer spacer) { + spacer.getElement().setInnerText( + "Spacer for row " + spacer.getRow()); + } + }; + + @Override + public void execute() { + BodyRowContainer body = escalator.getBody(); + + if (body.getSpacerUpdater().equals(SpacerUpdater.NULL)) { + body.setSpacerUpdater(CUSTOM); + } else { + body.setSpacerUpdater(SpacerUpdater.NULL); + } + } + }, menupath); + createSpacersMenuForRow(1, menupath); createSpacersMenuForRow(50, menupath); } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java index 63286af86f..e7ebcfeb51 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java @@ -25,6 +25,7 @@ import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; import com.vaadin.client.widget.escalator.RowContainer; import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; +import com.vaadin.client.widget.escalator.SpacerUpdater; import com.vaadin.client.widgets.Escalator; import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget; @@ -112,6 +113,17 @@ public class EscalatorProxy extends Escalator { throws IllegalArgumentException { rowContainer.setSpacer(rowIndex, height); } + + @Override + public void setSpacerUpdater(SpacerUpdater spacerUpdater) + throws IllegalArgumentException { + rowContainer.setSpacerUpdater(spacerUpdater); + } + + @Override + public SpacerUpdater getSpacerUpdater() { + return rowContainer.getSpacerUpdater(); + } } private class RowContainerProxy implements RowContainer { -- cgit v1.2.3 From a7db9821bac2e41601dd4fd0f169d8a94d6e282e Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Fri, 13 Feb 2015 15:53:50 +0200 Subject: Client side event for column reordering in Grid. #16643 Change-Id: I40bb42c7a5a3e9f92051c03dc5fe91b83cd88db7 --- .../widget/grid/events/ColumnReorderEvent.java | 52 +++++++++++++++ .../widget/grid/events/ColumnReorderHandler.java | 41 ++++++++++++ client/src/com/vaadin/client/widgets/Grid.java | 18 ++++++ .../basicfeatures/GridColumnReorderEventTest.java | 74 ++++++++++++++++++++++ .../client/grid/GridBasicClientFeaturesWidget.java | 45 +++++++++++++ 5 files changed, 230 insertions(+) create mode 100644 client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java create mode 100644 client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java create mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java (limited to 'client') 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..4890ed063d --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java @@ -0,0 +1,52 @@ +/* + * 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's columns have been + * reordered. + * + * @param + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public class ColumnReorderEvent extends GwtEvent> { + + /** + * Handler type. + */ + private final static Type> TYPE = new Type>(); + + public static final Type> getType() { + return TYPE; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Type> getAssociatedType() { + return (Type) TYPE; + } + + @Override + protected void dispatch(ColumnReorderHandler 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..e4f258088f --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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 + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public interface ColumnReorderHandler extends EventHandler { + + /** + * A column reorder event, fired by Grid when the columns of the Grid have + * been reordered. + * + * @since + * @param event + * column reorder event + */ + public void onColumnReorder(ColumnReorderEvent event); +} diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index b0e18cc2ab..643f223068 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -100,6 +100,8 @@ 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.FooterClickHandler; import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; @@ -6040,6 +6042,20 @@ public class Grid extends ResizeComposite implements return addHandler(handler, doubleClickEvent.getAssociatedType()); } + /** + * Register a column reorder handler to this Grid. The event for this + * handler is fired when the Grid's columns are reordered. + * + * @since + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnReorderHandler( + ColumnReorderHandler handler) { + return addHandler(handler, ColumnReorderEvent.getType()); + } + /** * Apply sorting to data source. */ @@ -6163,6 +6179,8 @@ public class Grid extends ResizeComposite implements for (FooterRow row : footer.getRows()) { row.calculateColspans(); } + + fireEvent(new ColumnReorderEvent()); } /** diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java new file mode 100644 index 0000000000..eda064284c --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.tests.annotations.TestCategory; + +/** + * + * @since + * @author Vaadin Ltd + */ +@TestCategory("grid") +public class GridColumnReorderEventTest extends GridBasicClientFeaturesTest { + + @Before + public void before() { + openTestURL(); + } + + @Test + public void columnReorderEventTriggered() { + final int firstIndex = 3; + final int secondIndex = 4; + final String firstHeaderText = getGridElement().getHeaderCell(0, + firstIndex).getText(); + final String secondHeaderText = getGridElement().getHeaderCell(0, + secondIndex).getText(); + selectMenuPath("Component", "Internals", "Listeners", + "Add ColumnReorder listener"); + selectMenuPath("Component", "Columns", "Column " + secondIndex, + "Move column left"); + // columns 3 and 4 should have swapped to 4 and 3 + GridCellElement headerCell = getGridElement().getHeaderCell(0, + firstIndex); + assertEquals(secondHeaderText, headerCell.getText()); + headerCell = getGridElement().getHeaderCell(0, secondIndex); + assertEquals(firstHeaderText, headerCell.getText()); + + // the reorder event should have typed the order to this label + WebElement columnReorderElement = findElement(By.id("columnreorder")); + int eventIndex = Integer.parseInt(columnReorderElement + .getAttribute("columns")); + assertEquals(1, eventIndex); + + // trigger another event + selectMenuPath("Component", "Columns", "Column " + secondIndex, + "Move column left"); + columnReorderElement = findElement(By.id("columnreorder")); + eventIndex = Integer.parseInt(columnReorderElement + .getAttribute("columns")); + assertEquals(2, eventIndex); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index bfe8b3cf4d..265a93fb59 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -55,6 +55,8 @@ import com.vaadin.client.widget.grid.datasources.ListSorter; import com.vaadin.client.widget.grid.events.BodyKeyDownHandler; import com.vaadin.client.widget.grid.events.BodyKeyPressHandler; import com.vaadin.client.widget.grid.events.BodyKeyUpHandler; +import com.vaadin.client.widget.grid.events.ColumnReorderEvent; +import com.vaadin.client.widget.grid.events.ColumnReorderHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; import com.vaadin.client.widget.grid.events.FooterKeyPressHandler; import com.vaadin.client.widget.grid.events.FooterKeyUpHandler; @@ -444,6 +446,31 @@ public class GridBasicClientFeaturesWidget extends }); } }, listenersPath); + addMenuCommand("Add ColumnReorder listener", new ScheduledCommand() { + private HandlerRegistration columnReorderHandler = null; + + @Override + public void execute() { + if (columnReorderHandler != null) { + return; + } + final Label columnOrderLabel = new Label(); + columnOrderLabel.getElement().setId("columnreorder"); + addLineEnd(columnOrderLabel, 300); + columnReorderHandler = grid + .addColumnReorderHandler(new ColumnReorderHandler>() { + + private int eventIndex = 0; + + @Override + public void onColumnReorder( + ColumnReorderEvent> event) { + columnOrderLabel.getElement().setAttribute( + "columns", "" + (++eventIndex)); + } + }); + } + }, listenersPath); } private void createStateMenu() { @@ -768,6 +795,24 @@ public class GridBasicClientFeaturesWidget extends }); } }, "Component", "Columns", "Column " + i); + addMenuCommand("Move column left", new ScheduledCommand() { + + @SuppressWarnings("unchecked") + @Override + public void execute() { + List>> cols = grid.getColumns(); + ArrayList reordered = new ArrayList(cols); + if (index == 0) { + Column> col = reordered.remove(0); + reordered.add(col); + } else { + Column> col = reordered.remove(index); + reordered.add(index - 1, col); + } + grid.setColumnOrder(reordered.toArray(new Column[reordered + .size()])); + } + }, "Component", "Columns", "Column " + i); } } -- cgit v1.2.3 From f5f699e61cd42f5c4ff1bc9b63ec2db8d446e316 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 16 Feb 2015 17:05:40 +0200 Subject: Adds initial details row support to Grid (#16644) For now, the details row only supports text content and index based row references Change-Id: Id12557d83732475b9a5133b3ee32ae31ba4fcdd7 --- .../VAADIN/themes/base/escalator/escalator.scss | 4 +- WebContent/VAADIN/themes/base/grid/grid.scss | 4 + .../client/widget/grid/DetailsGenerator.java | 45 ++++++++++ client/src/com/vaadin/client/widgets/Grid.java | 100 ++++++++++++++++++++- .../client/grid/GridBasicClientFeaturesWidget.java | 40 +++++++++ 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 client/src/com/vaadin/client/widget/grid/DetailsGenerator.java (limited to 'client') diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss index 6d146f3a74..7949b52882 100644 --- a/WebContent/VAADIN/themes/base/escalator/escalator.scss +++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss @@ -137,9 +137,7 @@ position: absolute; display: block; - // debug - background-color: rgba(0,0,0,0.6); - color: white; + background-color: $background-color; > td { width: 100%; diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss index e4a4a1d920..730bee8a6b 100644 --- a/WebContent/VAADIN/themes/base/grid/grid.scss +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -328,6 +328,10 @@ $v-grid-editor-background-color: $v-grid-row-background-color !default; .#{$primaryStyleName}-editor-save { margin-right: 4px; } + + .#{$primaryStyleName}-spacer { + border: $v-grid-border; + } // Renderers 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..665362ca8e --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid; + +/** + * A callback interface for generating details for a particular row in Grid. + * + * @since + * @author Vaadin Ltd + */ +public interface DetailsGenerator { + + public static final DetailsGenerator NULL = new DetailsGenerator() { + @Override + public String 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 null to leave the + * details empty. + */ + // TODO: provide a row object instead of index (maybe, needs discussion?) + // TODO: return a Widget instead of a String + String getDetails(int rowIndex); +} diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 71962d6953..31984f7d5b 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -36,6 +36,7 @@ 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.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; @@ -77,10 +78,13 @@ 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.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; @@ -1776,6 +1780,8 @@ public class Grid extends ResizeComposite implements private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle"; + private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; + private EventCellReference eventCell = new EventCellReference(this); private GridKeyDownEvent keyDown = new GridKeyDownEvent(this, eventCell); private GridKeyUpEvent keyUp = new GridKeyUpEvent(this, eventCell); @@ -2768,6 +2774,38 @@ public class Grid extends ResizeComposite implements } } + private class GridSpacerUpdater implements SpacerUpdater { + @Override + public void init(Spacer spacer) { + int rowIndex = spacer.getRow(); + + String string = detailsGenerator.getDetails(rowIndex); + if (string == null) { + destroy(spacer); + escalator.getBody().setSpacer(rowIndex, + DETAILS_ROW_INITIAL_HEIGHT); + return; + } + + spacer.getElement().setInnerText(string); + + /* + * Once we have the content properly inside the DOM, we should + * re-measure it to make sure that it's the correct height. + */ + double measuredHeight = WidgetUtil + .getRequiredHeightBoundingClientRectDouble(spacer + .getElement()); + assert getElement().isOrHasChild(spacer.getElement()) : "The spacer element wasn't in the DOM during measurement, but was assumed to be."; + escalator.getBody().setSpacer(rowIndex, measuredHeight); + } + + @Override + public void destroy(Spacer spacer) { + spacer.getElement().setInnerText(""); + } + } + /** * Escalator used internally by grid to render the rows */ @@ -2853,6 +2891,10 @@ public class Grid extends ResizeComposite implements private boolean enabled = true; + private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; + + private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater(); + /** * Enumeration for easy setting of selection mode. */ @@ -4902,7 +4944,7 @@ public class Grid extends ResizeComposite implements EventTarget target = event.getEventTarget(); - if (!Element.is(target)) { + if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) { return; } @@ -4968,6 +5010,19 @@ public class Grid extends ResizeComposite implements } } + private boolean isOrContainsInSpacer(Node node) { + Node n = node; + while (n != null && n != getElement()) { + if (Element.is(n) + && Element.as(n).getClassName() + .equals(getStylePrimaryName() + "-spacer")) { + return true; + } + n = n.getParentNode(); + } + return false; + } + private boolean isElementInChildWidget(Element e) { Widget w = WidgetUtil.findWidget(e, null); @@ -6279,4 +6334,47 @@ public class Grid extends ResizeComposite implements public void resetSizesFromDom() { getEscalator().resetSizesFromDom(); } + + /** + * Sets a new details generator for row details. + *

+ * The currently opened row details will be re-rendered. + * + * @since + * @param detailsGenerator + * the details generator to set + */ + public void setDetailsGenerator(DetailsGenerator detailsGenerator) + throws IllegalArgumentException { + + this.detailsGenerator = detailsGenerator; + + // this will refresh all visible spacers + escalator.getBody().setSpacerUpdater(gridSpacerUpdater); + } + + /** + * Gets the current details generator for row details. + * + * @since + * @return the detailsGenerator the current details generator + */ + public DetailsGenerator getDetailsGenerator() { + return detailsGenerator; + } + + /** + * Shows or hides the details for a specific row. + * + * @since + * @param row + * the index of the affected row + * @param visible + * true to show the details, or false + * to hide them + */ + public void setDetailsVisible(int rowIndex, boolean visible) { + double height = visible ? DETAILS_ROW_INITIAL_HEIGHT : -1; + escalator.getBody().setSpacer(rowIndex, height); + } } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index 0452aa65d1..7c2ca3eedb 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -46,6 +46,7 @@ import com.vaadin.client.renderers.TextRenderer; import com.vaadin.client.ui.VLabel; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; +import com.vaadin.client.widget.grid.DetailsGenerator; import com.vaadin.client.widget.grid.EditorHandler; import com.vaadin.client.widget.grid.RendererCellReference; import com.vaadin.client.widget.grid.RowReference; @@ -400,6 +401,7 @@ public class GridBasicClientFeaturesWidget extends createEditorMenu(); createInternalsMenu(); createDataSourceMenu(); + createDetailsMenu(); grid.getElement().getStyle().setZIndex(0); @@ -1214,4 +1216,42 @@ public class GridBasicClientFeaturesWidget extends String coords = "(" + object + ", " + column + ")"; label.setText(coords + " " + output); } + + private void createDetailsMenu() { + String[] menupath = new String[] { "Component", "Row details" }; + addMenuCommand("Set generator", new ScheduledCommand() { + @Override + public void execute() { + grid.setDetailsGenerator(new DetailsGenerator() { + @Override + public String getDetails(int rowIndex) { + return "Row: " + rowIndex + ". Lorem ipsum " + + "dolor sit amet, consectetur adipiscing " + + "elit. Morbi congue massa non augue " + + "pulvinar, nec consectetur justo efficitur. " + + "In nec arcu sit amet lorem hendrerit " + + "mollis."; + } + }); + } + }, menupath); + addMenuCommand("Toggle details for row 1", new ScheduledCommand() { + boolean visible = false; + + @Override + public void execute() { + visible = !visible; + grid.setDetailsVisible(1, visible); + } + }, menupath); + addMenuCommand("Toggle details for row 100", new ScheduledCommand() { + boolean visible = false; + + @Override + public void execute() { + visible = !visible; + grid.setDetailsVisible(100, visible); + } + }, menupath); + } } -- cgit v1.2.3 From add05e15fcd82d73f4c46245dbe6d0999437cdec Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Tue, 17 Feb 2015 11:51:11 +0200 Subject: Modifies GridElement to support details (#16644) Grid's SubPartAware logic was refactored, splitting it into both Grid and Escalator. Also adds tests for grid details rows. Change-Id: I4876a8a9a397eea35526e15f7e447c69b0d96983 --- .../src/com/vaadin/client/widgets/Escalator.java | 240 ++++++++++++++++++++- client/src/com/vaadin/client/widgets/Grid.java | 169 ++++----------- .../com/vaadin/testbench/elements/GridElement.java | 38 ++-- .../grid/basicfeatures/client/GridDetailsTest.java | 99 +++++++++ 4 files changed, 410 insertions(+), 136 deletions(-) create mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 6c6998277f..567262c6b2 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -16,6 +16,7 @@ package com.vaadin.client.widgets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -57,6 +58,7 @@ 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; @@ -267,7 +269,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 /* @@ -4319,6 +4322,26 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker 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"; } + + 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) { + getLogger().warning("SpacerContainer.getSubPartElement " + index); + + SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index)); + if (spacer != null) { + return spacer.getElement(); + } else { + return null; + } + } } private class ElementPositionBookkeeper { @@ -4347,6 +4370,50 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + 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 @@ -5295,4 +5362,175 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker int to = (int) Math.ceil(body.heightOfSection); return Range.between(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 containers = Arrays.asList(getHeader(), getBody(), + getFooter()); + List 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 31984f7d5b..7890b13904 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -130,6 +130,7 @@ 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.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.GridConstants; @@ -5250,153 +5251,77 @@ public class Grid 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; - } + Element subPartElement = escalator.getSubPartElement(subPart + .replaceFirst("^details\\[", "spacer[")); + if (subPartElement != null) { + return DOM.asOld(subPartElement); } - 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()); - } - } + SubPartArguments args = Escalator.parseSubPartArguments(subPart); + + Element editor = getSubPartElementEditor(args); + if (editor != null) { + return DOM.asOld(editor); } + 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); - } + private Element getSubPartElementEditor(SubPartArguments args) { - targetElement = getCellFromRow(TableRowElement.as(targetElement), - indices[1]); - - for (int i = 2; i < indices.length && targetElement != null; ++i) { - targetElement = (Element) targetElement.getChild(indices[i]); - } + if (!args.getType().equalsIgnoreCase("editor") + || editor.getState() != State.ACTIVE) { + return null; } - return targetElement; - } - - private Element getCellFromRow(TableRowElement rowElement, int index) { - int childCount = rowElement.getCells().getLength(); - if (index < 0 || index >= childCount) { - return null; + 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(); } - TableCellElement currentCell = null; - boolean indexInColspan = false; - int i = 0; + return null; + } - while (!indexInColspan) { - currentCell = rowElement.getCells().getItem(i); + @Override + @SuppressWarnings("deprecation") + public String getSubPartName(com.google.gwt.user.client.Element subElement) { - // Calculate if this is the cell we are looking for - int colSpan = currentCell.getColSpan(); - indexInColspan = index < colSpan + i; + String escalatorStructureName = escalator.getSubPartName(subElement); + if (escalatorStructureName != null) { + return escalatorStructureName.replaceFirst("^spacer", "details"); + } - // Increment by colspan to skip over hidden cells - i += colSpan; + String editorName = getSubPartNameEditor(subElement); + if (editorName != null) { + return editorName; } - return currentCell; - } - @Override - public String getSubPartName(com.google.gwt.user.client.Element subElement) { - // Containers and matching SubPart types - List containers = Arrays.asList(escalator.getHeader(), - escalator.getBody(), escalator.getFooter()); - List containerType = Arrays.asList("header", "cell", "footer"); + return null; + } - 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()); - } + private String getSubPartNameEditor(Element subElement) { - 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() + "]"); - } + if (editor.getState() != State.ACTIVE + || !editor.editorOverlay.isOrHasChild(subElement)) { + return null; } - // Check if subelement is part of editor. - if (editor.getState() == State.ACTIVE) { - if (editor.editorOverlay.isOrHasChild(subElement)) { - int i = 0; - for (Column column : columns) { - if (editor.getWidget(column).getElement() - .isOrHasChild(subElement)) { - return "editor[" + i + "]"; - } - ++i; - } - return "editor"; + int i = 0; + for (Column column : columns) { + if (editor.getWidget(column).getElement().isOrHasChild(subElement)) { + return "editor[" + i + "]"; } + ++i; } - return null; + return "editor"; } private void setSelectColumnRenderer( diff --git a/uitest/src/com/vaadin/testbench/elements/GridElement.java b/uitest/src/com/vaadin/testbench/elements/GridElement.java index 3753696855..ff7343a3d6 100644 --- a/uitest/src/com/vaadin/testbench/elements/GridElement.java +++ b/uitest/src/com/vaadin/testbench/elements/GridElement.java @@ -25,7 +25,7 @@ import com.vaadin.testbench.By; import com.vaadin.testbench.TestBenchElement; /** - * TestBench Element API for Grid + * TestBench Element API for Grid. * * @since * @author Vaadin Ltd @@ -148,7 +148,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Scrolls Grid element so that wanted row is displayed + * Scrolls Grid element so that wanted row is displayed. * * @param index * Target row @@ -251,7 +251,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get header row count + * Get header row count. * * @return Header row count */ @@ -260,7 +260,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get footer row count + * Get footer row count. * * @return Footer row count */ @@ -269,7 +269,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get a header row by index + * Get a header row by index. * * @param rowIndex * Row index @@ -280,7 +280,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get a footer row by index + * Get a footer row by index. * * @param rowIndex * Row index @@ -291,7 +291,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get the vertical scroll element + * Get the vertical scroll element. * * @return The element representing the vertical scrollbar */ @@ -301,7 +301,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get the horizontal scroll element + * Get the horizontal scroll element. * * @return The element representing the horizontal scrollbar */ @@ -311,7 +311,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get the header element + * Get the header element. * * @return The thead element */ @@ -320,7 +320,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get the body element + * Get the body element. * * @return the tbody element */ @@ -329,7 +329,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get the footer element + * Get the footer element. * * @return the tfoot element */ @@ -338,7 +338,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Get the element wrapping the table element + * Get the element wrapping the table element. * * @return The element that wraps the table element */ @@ -353,7 +353,7 @@ public class GridElement extends AbstractComponentElement { } /** - * Helper function to get Grid subparts wrapped correctly + * Helper function to get Grid subparts wrapped correctly. * * @param subPartSelector * SubPart to be used in ComponentLocator @@ -362,4 +362,16 @@ public class GridElement extends AbstractComponentElement { private TestBenchElement getSubPart(String subPartSelector) { return (TestBenchElement) findElement(By.vaadin(subPartSelector)); } + + /** + * Gets the element that contains the details of a row. + * + * @since + * @param rowIndex + * the index of the row for the details + * @return the element that contains the details of a row + */ + public TestBenchElement getDetails(int rowIndex) { + return getSubPart("#details[" + rowIndex + "]"); + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java new file mode 100644 index 0000000000..981a1cc217 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.client; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.NoSuchElementException; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest; + +public class GridDetailsTest extends GridBasicClientFeaturesTest { + + private static final String[] SET_GENERATOR = new String[] { "Component", + "Row details", "Set generator" }; + private static final String[] TOGGLE_DETAILS_FOR_ROW_1 = new String[] { + "Component", "Row details", "Toggle details for row 1" }; + private static final String[] TOGGLE_DETAILS_FOR_ROW_100 = new String[] { + "Component", "Row details", "Toggle details for row 100" }; + + @Before + public void setUp() { + openTestURL(); + } + + @Test(expected = NoSuchElementException.class) + public void noDetailsByDefault() { + assertNull("details for row 1 should not exist at the start", + getGridElement().getDetails(1)); + } + + @Test + public void nullRendererShowsDetailsPlaceholder() { + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + TestBenchElement details = getGridElement().getDetails(1); + assertNotNull("details for row 1 should not exist at the start", + details); + assertTrue("details should've been empty for null renderer", details + .getText().isEmpty()); + } + + @Test + public void applyRendererThenOpenDetails() { + selectMenuPath(SET_GENERATOR); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + + TestBenchElement details = getGridElement().getDetails(1); + assertTrue("Unexpected details content", + details.getText().startsWith("Row: 1.")); + } + + @Test + public void openDetailsThenAppyRenderer() { + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + selectMenuPath(SET_GENERATOR); + + TestBenchElement details = getGridElement().getDetails(1); + assertTrue("Unexpected details content", + details.getText().startsWith("Row: 1.")); + } + + @Test + public void openHiddenDetailsThenScrollToIt() { + try { + getGridElement().getDetails(100); + fail("details row for 100 was apparently found, while it shouldn't have been."); + } catch (NoSuchElementException e) { + // expected + } + + selectMenuPath(SET_GENERATOR); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_100); + + // scroll a bit beyond so we see below. + getGridElement().scrollToRow(101); + + TestBenchElement details = getGridElement().getDetails(100); + assertTrue("Unexpected details content", + details.getText().startsWith("Row: 100.")); + } +} -- cgit v1.2.3 From e44ab1ae58b6d622737935b01a2ddacab1661e5a Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Tue, 17 Feb 2015 17:25:33 +0200 Subject: Adds Widget support for DetailsGenerator (#16644) Change-Id: Ib964b2aa102b8c56e65b0af87bed008248038599 --- .../client/widget/grid/DetailsGenerator.java | 7 +- client/src/com/vaadin/client/widgets/Grid.java | 58 +++++++++++++-- .../grid/basicfeatures/client/GridDetailsTest.java | 85 ++++++++++++++++++++++ .../client/grid/GridBasicClientFeaturesWidget.java | 60 +++++++++++++-- 4 files changed, 195 insertions(+), 15 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java index 665362ca8e..264aa4e614 100644 --- a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java +++ b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java @@ -15,6 +15,8 @@ */ 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. * @@ -25,7 +27,7 @@ public interface DetailsGenerator { public static final DetailsGenerator NULL = new DetailsGenerator() { @Override - public String getDetails(int rowIndex) { + public Widget getDetails(int rowIndex) { return null; } }; @@ -40,6 +42,5 @@ public interface DetailsGenerator { * details empty. */ // TODO: provide a row object instead of index (maybe, needs discussion?) - // TODO: return a Widget instead of a String - String getDetails(int rowIndex); + Widget getDetails(int rowIndex); } diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 7890b13904..c0936bccec 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -2776,19 +2776,44 @@ public class Grid extends ResizeComposite implements } private class GridSpacerUpdater implements SpacerUpdater { + + private final Map elementToWidgetMap = new HashMap(); + @Override public void init(Spacer spacer) { + + assert spacer.getElement().getFirstChild() == null : "The spacer's" + + " element should be empty at this point. (row: " + + spacer.getRow() + ", child: " + + spacer.getElement().getFirstChild() + ")"; + int rowIndex = spacer.getRow(); - String string = detailsGenerator.getDetails(rowIndex); - if (string == null) { - destroy(spacer); + Widget detailsWidget = null; + try { + detailsWidget = detailsGenerator.getDetails(rowIndex); + } catch (Throwable e) { + getLogger().log( + Level.SEVERE, + "Exception while generating details for row " + + rowIndex, e); + } + + if (detailsWidget == null) { + spacer.getElement().removeAllChildren(); escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT); return; } - spacer.getElement().setInnerText(string); + Element element = detailsWidget.getElement(); + spacer.getElement().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 @@ -2803,7 +2828,30 @@ public class Grid extends ResizeComposite implements @Override public void destroy(Spacer spacer) { - spacer.getElement().setInnerText(""); + + assert getElement().isOrHasChild(spacer.getElement()) : "Trying " + + "to destroy a spacer that is not connected to this " + + "Grid's DOM. (row: " + spacer.getRow() + ", element: " + + spacer.getElement() + ")"; + + Widget detailsWidget = elementToWidgetMap.remove(spacer + .getElement().getFirstChildElement()); + + if (detailsWidget != null) { + /* + * The widget may be null here if the previous generator + * returned a null widget. + */ + + assert spacer.getElement().getFirstChild() != null : "The " + + "details row to destroy did not contain a widget - " + + "probably removed by something else without " + + "permission? (row: " + spacer.getRow() + + ", element: " + spacer.getElement() + ")"; + + setParent(detailsWidget, null); + spacer.getElement().removeAllChildren(); + } } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java index 981a1cc217..5e8793c964 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridDetailsTest.java @@ -15,6 +15,9 @@ */ package com.vaadin.tests.components.grid.basicfeatures.client; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -23,14 +26,22 @@ import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Test; import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import com.vaadin.testbench.By; +import com.vaadin.testbench.ElementQuery; import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elements.NotificationElement; import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest; public class GridDetailsTest extends GridBasicClientFeaturesTest { private static final String[] SET_GENERATOR = new String[] { "Component", "Row details", "Set generator" }; + private static final String[] SET_FAULTY_GENERATOR = new String[] { + "Component", "Row details", "Set faulty generator" }; + private static final String[] SET_EMPTY_GENERATOR = new String[] { + "Component", "Row details", "Set empty generator" }; private static final String[] TOGGLE_DETAILS_FOR_ROW_1 = new String[] { "Component", "Row details", "Toggle details for row 1" }; private static final String[] TOGGLE_DETAILS_FOR_ROW_100 = new String[] { @@ -38,6 +49,7 @@ public class GridDetailsTest extends GridBasicClientFeaturesTest { @Before public void setUp() { + setDebug(true); openTestURL(); } @@ -96,4 +108,77 @@ public class GridDetailsTest extends GridBasicClientFeaturesTest { assertTrue("Unexpected details content", details.getText().startsWith("Row: 100.")); } + + @Test + public void errorUpdaterShowsErrorNotification() { + assertFalse("No notifications should've been at the start", + $(NotificationElement.class).exists()); + + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + selectMenuPath(SET_FAULTY_GENERATOR); + + ElementQuery notification = $(NotificationElement.class); + assertTrue("Was expecting an error notification here", + notification.exists()); + notification.first().closeNotification(); + + assertEquals("The error details element should be empty", "", + getGridElement().getDetails(1).getText()); + } + + @Test + public void updaterStillWorksAfterError() { + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + + selectMenuPath(SET_FAULTY_GENERATOR); + $(NotificationElement.class).first().closeNotification(); + selectMenuPath(SET_GENERATOR); + + assertNotEquals( + "New details should've been generated even after error", "", + getGridElement().getDetails(1).getText()); + } + + @Test + public void updaterRendersExpectedWidgets() { + selectMenuPath(SET_GENERATOR); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + + TestBenchElement detailsElement = getGridElement().getDetails(1); + assertNotNull(detailsElement.findElement(By.className("gwt-Label"))); + assertNotNull(detailsElement.findElement(By.className("gwt-Button"))); + } + + @Test + public void widgetsInUpdaterWorkAsExpected() { + selectMenuPath(SET_GENERATOR); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + + TestBenchElement detailsElement = getGridElement().getDetails(1); + WebElement button = detailsElement.findElement(By + .className("gwt-Button")); + button.click(); + + WebElement label = detailsElement + .findElement(By.className("gwt-Label")); + assertEquals("clicked", label.getText()); + } + + @Test + public void emptyGenerator() { + selectMenuPath(SET_EMPTY_GENERATOR); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + + assertEquals("empty generator did not produce an empty details row", + "", getGridElement().getDetails(1).getText()); + } + + @Test(expected = NoSuchElementException.class) + public void removeDetailsRow() { + selectMenuPath(SET_GENERATOR); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + selectMenuPath(TOGGLE_DETAILS_FOR_ROW_1); + + getGridElement().getDetails(1); + } } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index 7c2ca3eedb..110b14c721 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -33,9 +33,11 @@ import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.data.DataSource; import com.vaadin.client.data.DataSource.RowHandle; import com.vaadin.client.renderers.DateRenderer; @@ -1224,17 +1226,59 @@ public class GridBasicClientFeaturesWidget extends public void execute() { grid.setDetailsGenerator(new DetailsGenerator() { @Override - public String getDetails(int rowIndex) { - return "Row: " + rowIndex + ". Lorem ipsum " - + "dolor sit amet, consectetur adipiscing " - + "elit. Morbi congue massa non augue " - + "pulvinar, nec consectetur justo efficitur. " - + "In nec arcu sit amet lorem hendrerit " - + "mollis."; + public Widget getDetails(int rowIndex) { + FlowPanel panel = new FlowPanel(); + + final Label label = new Label("Row: " + rowIndex + "."); + Button button = new Button("Button", + new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + label.setText("clicked"); + } + }); + + panel.add(label); + panel.add(button); + return panel; } }); } }, menupath); + + addMenuCommand("Set faulty generator", new ScheduledCommand() { + @Override + public void execute() { + grid.setDetailsGenerator(new DetailsGenerator() { + @Override + public Widget getDetails(int rowIndex) { + throw new RuntimeException("This is by design."); + } + }); + } + }, menupath); + + addMenuCommand("Set empty generator", new ScheduledCommand() { + @Override + public void execute() { + grid.setDetailsGenerator(new DetailsGenerator() { + /* + * While this is functionally equivalent to the NULL + * generator, it's good to be explicit, since the behavior + * isn't strictly tied between them. NULL generator might be + * changed to render something different by default, and an + * empty generator might behave differently also in the + * future. + */ + + @Override + public Widget getDetails(int rowIndex) { + return null; + } + }); + } + }, menupath); + addMenuCommand("Toggle details for row 1", new ScheduledCommand() { boolean visible = false; @@ -1244,6 +1288,7 @@ public class GridBasicClientFeaturesWidget extends grid.setDetailsVisible(1, visible); } }, menupath); + addMenuCommand("Toggle details for row 100", new ScheduledCommand() { boolean visible = false; @@ -1253,5 +1298,6 @@ public class GridBasicClientFeaturesWidget extends grid.setDetailsVisible(100, visible); } }, menupath); + } } -- cgit v1.2.3 From 9b2f51ca9ff48ad9484b8ee67770ab1a58352b35 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Wed, 18 Feb 2015 10:05:25 +0200 Subject: Update server side state when columns are reordered. (#16643) Change-Id: I96c65dbb96614a5f5782b747fb8588647211cf4b --- .../vaadin/client/connectors/GridConnector.java | 52 ++++++++++++++++---- server/src/com/vaadin/ui/Grid.java | 40 +++++++++++++++ .../com/vaadin/shared/ui/grid/GridServerRpc.java | 12 +++++ .../grid/basicfeatures/GridBasicFeatures.java | 42 ++++++++++++++++ .../server/GridColumnReorderTest.java | 57 ++++++++++++++++++++++ 5 files changed, 194 insertions(+), 9 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index f263b47642..2ae04b2be2 100644 --- a/client/src/com/vaadin/client/connectors/GridConnector.java +++ b/client/src/com/vaadin/client/connectors/GridConnector.java @@ -50,6 +50,8 @@ 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.GridClickEvent; import com.vaadin.client.widget.grid.events.GridDoubleClickEvent; import com.vaadin.client.widget.grid.events.SelectAllEvent; @@ -360,6 +362,24 @@ public class GridConnector extends AbstractHasComponentsConnector implements } } + private ColumnReorderHandler columnReorderHandler = new ColumnReorderHandler() { + + @Override + public void onColumnReorder(ColumnReorderEvent event) { + if (!columnsUpdatedFromState) { + List> columns = getWidget().getColumns(); + final List newColumnOrder = new ArrayList(); + for (Column column : columns) { + newColumnOrder.add(((CustomGridColumn) column).id); + } + getRpcProxy(GridServerRpc.class).columnsReordered( + newColumnOrder, columnOrder); + columnOrder = newColumnOrder; + getState().columnOrder = newColumnOrder; + } + } + }; + /** * Maps a generated column id to a grid column instance */ @@ -370,13 +390,22 @@ public class GridConnector extends AbstractHasComponentsConnector implements private List columnOrder = new ArrayList(); /** - * 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; @@ -386,7 +415,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)); } @@ -398,7 +427,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements getRpcProxy(GridServerRpc.class).select( new ArrayList(selectedKeys)); } else { - updatedFromState = false; + selectionUpdatedFromState = false; } } }; @@ -496,6 +525,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements }); getWidget().setEditorHandler(new CustomEditorHandler()); + + getWidget().addColumnReorderHandler(columnReorderHandler); + getLayoutManager().registerDependency(this, getWidget().getElement()); layout(); } @@ -589,7 +621,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements columns[i] = columnIdToColumn.get(id); i++; } + columnsUpdatedFromState = true; getWidget().setColumnOrder(columns); + columnsUpdatedFromState = false; columnOrder = stateColumnOrder; } @@ -886,7 +920,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(getWidget(), (List) null, null, false)); diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index 764960606a..72a772713f 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -18,6 +18,7 @@ package com.vaadin.ui; import java.io.Serializable; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -70,6 +71,7 @@ import com.vaadin.event.SortEvent.SortListener; import com.vaadin.event.SortEvent.SortNotifier; import com.vaadin.server.AbstractClientConnector; import com.vaadin.server.AbstractExtension; +import com.vaadin.server.EncodeResult; import com.vaadin.server.ErrorMessage; import com.vaadin.server.JsonCodec; import com.vaadin.server.KeyMapper; @@ -3088,6 +3090,44 @@ public class Grid extends AbstractComponent implements SelectionNotifier, fireEvent(new ItemClickEvent(Grid.this, item, itemId, propertyId, details)); } + + @Override + public void columnsReordered(List newColumnOrder, + List oldColumnOrder) { + final String diffStateKey = "columnOrder"; + ConnectorTracker connectorTracker = getUI() + .getConnectorTracker(); + JsonObject diffState = connectorTracker.getDiffState(Grid.this); + // discard the change if the columns have been reordered from + // the server side, as the server side is always right + if (getState(false).columnOrder.equals(oldColumnOrder)) { + // Don't mark as dirty since client has the state already + getState(false).columnOrder = newColumnOrder; + // write changes to diffState so that possible reverting the + // column order is sent to client + assert diffState.hasKey(diffStateKey) : "Field name has changed"; + Type type = null; + try { + type = (getState(false).getClass().getDeclaredField( + diffStateKey).getGenericType()); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } + EncodeResult encodeResult = JsonCodec.encode( + getState(false).columnOrder, diffState, type, + connectorTracker); + + diffState.put(diffStateKey, encodeResult.getEncodedValue()); + // TODO fire column reorder event + } else { + // make sure the client is reverted to the order that the + // server thinks it is + diffState.remove(diffStateKey); + markAsDirty(); + } + } }); registerRpc(new EditorServerRpc() { diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java index c90a016383..4dec5530aa 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -47,4 +47,16 @@ public interface GridServerRpc extends ServerRpc { * mouse event details */ void itemClick(String rowKey, String columnId, MouseEventDetails details); + + /** + * Informs the server that the columns of the Grid have been reordered. + * + * @since + * @param newColumnOrder + * a list of column ids in the new order + * @param oldColumnOrder + * a list of column ids in order before the change + */ + void columnsReordered(List newColumnOrder, + List oldColumnOrder); } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java index 3a6aca11f2..792ef89d42 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java @@ -248,10 +248,25 @@ public class GridBasicFeatures extends AbstractComponentTest { addFilterActions(); + addInternalActions(); + this.grid = grid; return grid; } + private void addInternalActions() { + createClickAction("Update column order without updating client", + "Internals", new Command() { + @Override + public void execute(Grid grid, Void value, Object data) { + List columns = grid.getColumns(); + grid.setColumnOrder(columns.get(1).getPropertyId(), + columns.get(0).getPropertyId()); + grid.getUI().getConnectorTracker().markClean(grid); + } + }, null); + } + private void addFilterActions() { createClickAction("Column 1 starts with \"(23\"", "Filter", new Command() { @@ -658,6 +673,33 @@ public class GridBasicFeatures extends AbstractComponentTest { } } }, null, c); + createClickAction("Move left", getColumnProperty(c), + new Command() { + + @Override + public void execute(Grid grid, String value, Object data) { + final String columnProperty = getColumnProperty((Integer) data); + List cols = grid.getColumns(); + List reordered = new ArrayList(); + boolean addAsLast = false; + for (int i = 0; i < cols.size(); i++) { + Column col = cols.get(i); + if (col.getPropertyId().equals(columnProperty)) { + if (i == 0) { + addAsLast = true; + } else { + reordered.add(i - 1, columnProperty); + } + } else { + reordered.add(col.getPropertyId()); + } + } + if (addAsLast) { + reordered.add(columnProperty); + } + grid.setColumnOrder(reordered.toArray()); + } + }, null, c); createBooleanAction("Sortable", getColumnProperty(c), true, new Command() { diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java index 7b62ff85f9..7f4e9bb7e9 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java @@ -70,6 +70,26 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertColumnHeaderOrder(1, 2, 0); } + @Test + public void testColumnReordering_reorderingTwiceBackForth_reordered() { + // given + openTestURL(); + assertColumnHeaderOrder(0, 1, 2); + toggleColumnReordering(); + + // when + dragDefaultColumnHeader(2, 0, 10); + + // then + assertColumnHeaderOrder(2, 0, 1, 3); + + // when + dragDefaultColumnHeader(1, 3, 110); + + // then + assertColumnHeaderOrder(2, 1, 3, 0); + } + @Test public void testColumnReordering_notEnabled_noReordering() { // given @@ -83,10 +103,47 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertColumnHeaderOrder(0, 1, 2); } + @Test + public void testColumnReordering_userChangesRevertedByServer_columnsAreUpdated() { + // given + openTestURL(); + assertColumnHeaderOrder(0, 1, 2); + toggleColumnReordering(); + + // when + dragDefaultColumnHeader(0, 2, 10); + assertColumnHeaderOrder(1, 0, 2); + moveColumnManuallyLeftByOne(0); + + // then + assertColumnHeaderOrder(0, 1, 2); + } + + @Test + public void testColumnReordering_concurrentUpdatesFromServer_columnOrderFromServerUsed() { + // given + openTestURL(); + assertColumnHeaderOrder(0, 1, 2); + toggleColumnReordering(); + + // when + selectMenuPath(new String[] { "Component", "Internals", + "Update column order without updating client" }); + dragDefaultColumnHeader(2, 0, 10); + + // then + assertColumnHeaderOrder(1, 0, 2); + } + private void toggleColumnReordering() { selectMenuPath(COLUMN_REORDERING_PATH); } + private void moveColumnManuallyLeftByOne(int index) { + selectMenuPath(new String[] { "Component", "Columns", + "Column " + index, "Move left" }); + } + private void assertColumnHeaderOrder(int... indices) { List headers = getGridHeaderRowCells(); for (int i = 0; i < indices.length; i++) { -- cgit v1.2.3 From 70b564a6bbb8f1af667a41cee0ef2e00afd5682b Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Wed, 18 Feb 2015 16:09:10 +0200 Subject: Adding/removing spacers affects Escalator row positions (#16644) Note that this patch is only a first step of many. This patch only moves displayed rows when spacer dimensions change. Doing anything else afterwards (even scrolling) will probably not (and is not intended to) look good at this stage. Change-Id: I4de6abd932f587682840396df7a56a7df710af69 --- .../src/com/vaadin/client/widgets/Escalator.java | 176 ++++++++++++++++----- .../EscalatorBasicClientFeaturesTest.java | 7 +- .../escalator/EscalatorSpacerTest.java | 119 ++++++++++++++ 3 files changed, 257 insertions(+), 45 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 567262c6b2..1d3faee51b 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -52,6 +52,7 @@ import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.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; @@ -1692,6 +1693,11 @@ public class Escalator extends Widget implements RequiresResize, public void setColumnFrozen(int column, boolean frozen) { final NodeList childRows = root.getRows(); + if (column == 0 && this instanceof BodyRowContainer) { + // TODO + getLogger().warning("[[spacers]] freezing columns"); + } + for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); @@ -1712,6 +1718,11 @@ public class Escalator extends Widget implements RequiresResize, public void updateFreezePosition(int column, double scrollLeft) { final NodeList childRows = root.getRows(); + if (column == 0 && this instanceof BodyRowContainer) { + // TODO + getLogger().warning("[[spacers]] update freeze position"); + } + for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); @@ -2507,6 +2518,9 @@ public class Escalator extends Widget implements RequiresResize, } if (rowsWereMoved) { + // TODO + getLogger().warning("[[spacers]] rotating rows during scroll"); + fireRowVisibilityChangeEvent(); if (scroller.touchHandlerBundle.touches == 0) { @@ -2526,6 +2540,9 @@ public class Escalator extends Widget implements RequiresResize, return; } + // TODO + getLogger().warning("[[spacers]] inserting rows"); + /* * TODO: this method should probably only add physical rows, and not * populate them - let everything be populated as appropriate by the @@ -2814,6 +2831,9 @@ public class Escalator extends Widget implements RequiresResize, return; } + // TODO + getLogger().warning("[[spacers]] removing rows"); + final Range viewportRange = getVisibleRowRange(); final Range removedRowsRange = Range .withLength(index, numberOfRows); @@ -3238,6 +3258,7 @@ public class Escalator extends Widget implements RequiresResize, * 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()) { @@ -3341,6 +3362,10 @@ public class Escalator extends Widget implements RequiresResize, * instead. */ public void verifyEscalatorCount() { + + // TODO + getLogger().warning("[[spacers]] verifying escalator row count"); + /* * This method indeed has a smell very similar to paintRemoveRows * and paintInsertRows. @@ -3374,6 +3399,11 @@ public class Escalator extends Widget implements RequiresResize, if (neededEscalatorRowsDiff > 0) { // needs more + // TODO + getLogger().warning( + "[[spacers]] adding more rows while expanding the " + + "body section"); + /* * This is a workaround for the issue where we might be scrolled * to the bottom, and the widget expands beyond the content @@ -3444,6 +3474,11 @@ public class Escalator extends Widget implements RequiresResize, else if (neededEscalatorRowsDiff < 0) { // needs less + // TODO + getLogger().warning( + "[[spacers]] removing rows while shrinking the body " + + "section"); + final ListIterator iter = visualRowOrder .listIterator(visualRowOrder.size()); for (int i = 0; i < -neededEscalatorRowsDiff; i++) { @@ -3652,6 +3687,44 @@ public class Escalator extends Widget implements RequiresResize, public SpacerUpdater getSpacerUpdater() { return spacerContainer.getSpacerUpdater(); } + + private double calculateRowTop(int logicalIndex) { + double top = spacerContainer.getSpacerHeightsSumUntil(logicalIndex); + return top + (logicalIndex * getDefaultRowHeight()); + } + + public void shiftRowPositions(int row, double diff) { + for (TableRowElement tr : getVisibleRowsAfter(row)) { + setRowPosition(tr, 0, getRowTop(tr) + diff); + } + + if (row > getTopRowLogicalIndex()) { + // TODO + getLogger().warning( + "scrollbar compensation not yet implemented"); + } + } + + private List 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 sublist = visualRowOrder.subList( + fromIndex, toIndex); + return Collections.unmodifiableList(sublist); + } + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4136,17 +4209,23 @@ public class Escalator extends Widget implements RequiresResize, private TableCellElement spacerElement; private TableRowElement root; private final int rowIndex; + private double height = -1; + private boolean domHasBeenSetup = false; - public SpacerImpl(int rowIndex, double height) { + public SpacerImpl(int rowIndex) { this.rowIndex = rowIndex; - // Build DOM structure root = TableRowElement.as(DOM.createTR()); spacerElement = TableCellElement.as(DOM.createTD()); root.appendChild(spacerElement); root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); + } + + 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; - // Configure DOM structure setHeight(height); root.getStyle().setWidth(100, Unit.PCT); @@ -4162,18 +4241,25 @@ public class Escalator extends Widget implements RequiresResize, } public void setPosition(double x, double y) { - positions.set(root, x, y); + positions.set(getRootElement(), x, y); } public void setStylePrimaryName(String style) { UIObject.setStylePrimaryName(root, style + "-spacer"); } - public void setHeight(double newHeight) { - root.getStyle().setHeight(newHeight, Unit.PX); - getLogger().warning( - "spacer's height changed, but pushing rows out of " - + "the way not implemented yet"); + public void setHeight(double height) { + + assert height >= 0 : "Height must be more >= 0 (was " + height + + ")"; + + double diff = height - Math.max(0, this.height); + this.height = height; + root.getStyle().setHeight(height, Unit.PX); + + shiftSpacerPositions(getRow(), diff); + body.shiftRowPositions(getRow(), diff); + body.verifyEscalatorCount(); } @Override @@ -4185,6 +4271,15 @@ public class Escalator extends Widget implements RequiresResize, 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()); + } } private final TreeMap rowIndexToSpacer = new TreeMap(); @@ -4209,6 +4304,16 @@ public class Escalator extends Widget implements RequiresResize, } } + @SuppressWarnings("boxing") + public double getSpacerHeightsSumUntil(int logicalIndex) { + double heights = 0; + for (SpacerImpl spacer : rowIndexToSpacer.headMap(logicalIndex, + false).values()) { + heights += spacer.getHeight(); + } + return heights; + } + private boolean spacerExists(int rowIndex) { return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex)); } @@ -4216,15 +4321,16 @@ public class Escalator extends Widget implements RequiresResize, @SuppressWarnings("boxing") private void insertNewSpacer(int rowIndex, double height) { - SpacerImpl spacer = new SpacerImpl(rowIndex, height); + SpacerImpl spacer = new SpacerImpl(rowIndex); rowIndexToSpacer.put(rowIndex, spacer); - spacer.setPosition(0, getSpacerTop(rowIndex)); + spacer.setPosition(0, calculateSpacerTop(rowIndex)); TableRowElement spacerRoot = spacer.getRootElement(); spacerRoot.getStyle().setWidth( columnConfiguration.calculateRowWidth(), Unit.PX); body.getElement().appendChild(spacerRoot); + spacer.setupDom(height); initSpacerContent(spacer); } @@ -4237,29 +4343,6 @@ public class Escalator extends Widget implements RequiresResize, return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); } - private double getSpacerTop(int rowIndex) { - double spacerHeights = 0; - - /*- - * TODO: uncomment this bit once the spacers start pushing the - * rows downwards, offseting indices. OTOH this entire method - * probably needs to be usable by BodyContainerImpl as well, - * since this same logic can/should be used for calculating the - * top position for rows. - - // Sum all spacer heights that occur before rowIndex. - for (Double spacerHeight : rowIndexToHeight.headMap( - Integer.valueOf(rowIndex), false).values()) { - spacerHeights += spacerHeight.doubleValue(); - } - */ - - double rowHeights = getBody().getDefaultRowHeight() - * (rowIndex + 1); - - return rowHeights + spacerHeights; - } - @SuppressWarnings("boxing") private void removeSpacer(int rowIndex) { SpacerImpl spacer = getSpacer(rowIndex); @@ -4333,8 +4416,6 @@ public class Escalator extends Widget implements RequiresResize, } public Element getSubPartElement(int index) { - getLogger().warning("SpacerContainer.getSubPartElement " + index); - SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index)); if (spacer != null) { return spacer.getElement(); @@ -4342,6 +4423,19 @@ public class Escalator extends Widget implements RequiresResize, return null; } } + + private double calculateSpacerTop(int logicalIndex) { + return body.calculateRowTop(logicalIndex) + + body.getDefaultRowHeight(); + } + + @SuppressWarnings("boxing") + private void shiftSpacerPositions(int changedRowIndex, double diffPx) { + for (SpacerImpl spacer : rowIndexToSpacer.tailMap(changedRowIndex, + false).values()) { + spacer.setPosition(0, spacer.getTop() + diffPx); + } + } } private class ElementPositionBookkeeper { @@ -5025,7 +5119,8 @@ public class Escalator extends Widget implements RequiresResize, /** * 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 @@ -5053,14 +5148,13 @@ public class Escalator extends Widget implements RequiresResize, } /** - * 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); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index b8bc644948..ef2d605928 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -240,17 +240,16 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest } protected void scrollVerticallyTo(int px) { - executeScript("arguments[0].scrollTop = " + px, getVeticalScrollbar()); + getVerticalScrollbar().scroll(px); } - private TestBenchElement getVeticalScrollbar() { + private TestBenchElement getVerticalScrollbar() { return (TestBenchElement) getEscalator().findElement( By.className("v-escalator-scroller-vertical")); } protected void scrollHorizontallyTo(int px) { - executeScript("arguments[0].scrollLeft = " + px, - getHorizontalScrollbar()); + getHorizontalScrollbar().scrollLeft(px); } private TestBenchElement getHorizontalScrollbar() { diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index 24e474a767..43a18b507d 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -15,16 +15,68 @@ */ package com.vaadin.tests.components.grid.basicfeatures.escalator; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.Before; import org.junit.Test; +import com.vaadin.testbench.TestBenchElement; import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { + //@formatter:off + // separate strings made so that eclipse can show the concatenated string by hovering the mouse over the constant + + // translate3d(0px, 40px, 123px); + // translate3d(24px, 15.251px, 0); + // translate(0, 40px); + private final static String TRANSLATE_VALUE_REGEX = + "translate(?:3d|)" // "translate" or "translate3d" + + "\\(" // literal "(" + + ".+?, " // the x argument, uninteresting + + "(" // start capturing the y argument + + "[0-9]+" // the integer part of the value + + "(?:" // start of the subpixel part of the value + + "\\.[0-9]" // if we have a period, there must be at least one number after it + + "[0-9]*" // any amount of accuracy afterwards is fine + + ")?" // the subpixel part is optional + + ")" + + "(?:px)?" // we don't care if the values are suffixed by "px" or not. + + "(?:, .*?)?" // the possible z argument, uninteresting (translate doesn't have one, translate3d does) + + "\\)" // literal ")" + + ";?"; // optional ending semicolon + + // 40px; + // 12.34px + private final static String TOP_VALUE_REGEX = + "(" // capture the pixel value + + "[0-9]+" // the pixel argument + + "(?:" // start of the subpixel part of the value + + "\\.[0-9]" // if we have a period, there must be at least one number after it + + "[0-9]*" // any amount of accuracy afterwards is fine + + ")?" // the subpixel part is optional + + ")" + + "(?:px)?" // optional "px" string + + ";?"; // optional semicolon + //@formatter:on + + private final static Pattern TRANSFORM_CSS_PATTERN = Pattern + .compile("transform: (.*?);"); // also matches "-webkit-transform"; + private final static Pattern TOP_CSS_PATTERN = Pattern + .compile("top: (.*?);"); + + private final static Pattern TRANSLATE_VALUE_PATTERN = Pattern + .compile(TRANSLATE_VALUE_REGEX); + private final static Pattern TOP_VALUE_PATTERN = Pattern + .compile(TOP_VALUE_REGEX); + @Before public void before() { openTestURL(); @@ -45,4 +97,71 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { assertNull("Spacer should not exist after removing it", getSpacer(1)); } + @Test + @SuppressWarnings("boxing") + public void spacerPushesVisibleRowsDown() { + double oldTop = getElementTop(getBodyRow(2)); + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + double newTop = getElementTop(getBodyRow(2)); + + assertGreater("Row below a spacer was not pushed down", newTop, oldTop); + } + + private static double getElementTop(TestBenchElement element) { + /* + * we need to parse the style attribute, since using getCssValue gets a + * normalized value that is harder to parse. + */ + String style = element.getAttribute("style"); + + String transform = getTransformFromStyle(style); + if (transform != null) { + return getTranslateYValue(transform); + } + + String top = getTopFromStyle(style); + if (top != null) { + return getTopValue(top); + } + + throw new IllegalArgumentException( + "Could not parse the top position from the CSS \"" + style + + "\""); + } + + private static String getTransformFromStyle(String style) { + return getFromStyle(TRANSFORM_CSS_PATTERN, style); + } + + private static String getTopFromStyle(String style) { + return getFromStyle(TOP_CSS_PATTERN, style); + } + + private static String getFromStyle(Pattern pattern, String style) { + Matcher matcher = pattern.matcher(style); + if (matcher.find()) { + assertEquals("wrong amount of groups matched in " + style, 1, + matcher.groupCount()); + return matcher.group(1); + } else { + return null; + } + } + + private static double getTranslateYValue(String translate) { + return getValueFromCss(TRANSLATE_VALUE_PATTERN, translate); + } + + private static double getTopValue(String top) { + return getValueFromCss(TOP_VALUE_PATTERN, top); + } + + private static double getValueFromCss(Pattern pattern, String css) { + Matcher matcher = pattern.matcher(css); + assertTrue("no matches for " + css + " against " + + TRANSLATE_VALUE_PATTERN, matcher.find()); + assertEquals("wrong amount of groups matched in " + css, 1, + matcher.groupCount()); + return Double.parseDouble(matcher.group(1)); + } } -- cgit v1.2.3 From bff2a3f558ea0416ae48b5595b59057a3475d6b6 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Thu, 19 Feb 2015 17:21:22 +0200 Subject: Theming and UX for Grid column reordering. (#16643) - Show sorting and focus on the dragged header. - Keep the focused header/cell the same after drag. - Make the drop marker get the same color as the grid selection. - Make dragged header and the drag element theme customizable - Valo related theming: little opacity, proper positioning Change-Id: Ia1c6f72ef2c7b4333e64ac410e50ef3688461749 --- WebContent/VAADIN/themes/base/grid/grid.scss | 5 +- .../VAADIN/themes/valo/components/_grid.scss | 9 + client/src/com/vaadin/client/widgets/Grid.java | 45 +++- .../grid/basicfeatures/GridBasicFeaturesTest.java | 44 ++++ .../basicfeatures/GridColumnReorderEventTest.java | 74 ------- .../grid/basicfeatures/GridColumnReorderTest.java | 233 +++++++++++++++++++++ .../server/GridColumnReorderTest.java | 32 --- 7 files changed, 326 insertions(+), 116 deletions(-) delete mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java create mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java (limited to 'client') diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss index d0bae911db..35024e27c0 100644 --- a/WebContent/VAADIN/themes/base/grid/grid.scss +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -14,6 +14,7 @@ $v-grid-row-focused-background-color: null !default; $v-grid-header-row-height: null !default; $v-grid-header-font-size: $v-font-size !default; $v-grid-header-background-color: $v-grid-row-background-color !default; +$v-grid-header-drag-marked-color: $v-grid-row-selected-background-color !default; $v-grid-footer-row-height: $v-grid-header-row-height !default; $v-grid-footer-font-size: $v-grid-header-font-size !default; @@ -61,12 +62,14 @@ $v-grid-editor-background-color: $v-grid-row-background-color !default; > .#{$primaryStyleName}-cell { border: $v-grid-border; + margin-top: -10px; opacity: 0.9; filter: alpha(opacity=90); // IE8 + z-index: 30000; } > .#{$primaryStyleName}-drop-marker { - background-color: #197de1; + background-color: $v-grid-header-drag-marked-color; position: absolute; width: 3px; } diff --git a/WebContent/VAADIN/themes/valo/components/_grid.scss b/WebContent/VAADIN/themes/valo/components/_grid.scss index 4cac9c5e43..0d6d2ff0a6 100644 --- a/WebContent/VAADIN/themes/valo/components/_grid.scss +++ b/WebContent/VAADIN/themes/valo/components/_grid.scss @@ -40,6 +40,15 @@ $v-grid-animations-enabled: $v-animations-enabled !default; text-shadow: valo-text-shadow($font-color: valo-font-color($v-grid-header-background-color), $background-color: $v-grid-header-background-color); } + .#{$primary-stylename}-header .#{$primary-stylename}-cell.dragged { + @include opacity(0.5, false); + @include transition (opacity .3s ease-in-out); + } + + .#{$primary-stylename}-header .#{$primary-stylename}-cell.dragged-column-header { + margin-top: round($v-grid-row-height/-2); + } + .#{$primary-stylename}-footer .#{$primary-stylename}-cell { @include valo-gradient($v-grid-footer-background-color); text-shadow: valo-text-shadow($font-color: valo-font-color($v-grid-footer-background-color), $background-color: $v-grid-footer-background-color); diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 643f223068..d54c023e3d 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -2932,10 +2932,6 @@ public class Grid extends ResizeComposite implements dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); } - private void resolveDragElementVerticalPosition() { - dragElement.getStyle().setTop(-10, Unit.PX); - } - private void resolveDragElementHorizontalPosition(final int clientX) { int left = clientX - table.getAbsoluteLeft(); left = Math.max(0, Math.min(left, table.getClientWidth())); @@ -2946,22 +2942,23 @@ public class Grid extends ResizeComposite implements @Override public void showDragElement() { initHeaderDragElementDOM(); - // TODO this clones also some unwanted style names, should confirm - // with UX what we want to show (focus/sort indicator) + // 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); - // might need to change this on fly once sorting with multiple - // header rows is possible - resolveDragElementVerticalPosition(); + // mark the column being dragged for styling + eventCell.getElement().addClassName("dragged"); + // mark the floating cell, for styling & testing + dragElement.addClassName("dragged-column-header"); } @Override public void removeDragElement() { table.removeFromParent(); dragElement.removeFromParent(); + eventCell.getElement().removeClassName("dragged"); } @Override @@ -2980,10 +2977,40 @@ public class Grid extends ResizeComposite implements @SuppressWarnings("unchecked") Column[] array = reordered.toArray(new Column[reordered .size()]); + transferCellFocusOnDrop(); setColumnOrder(array); } // else no reordering } + private void transferCellFocusOnDrop() { + final Cell focusedCell = cellFocusHandler.getFocusedCell(); + if (focusedCell != null) { + final int focusedCellColumnIndex = focusedCell.getColumn(); + final int focusedRowIndex = focusedCell.getRow(); + final int draggedColumnIndex = eventCell.getColumnIndex(); + // transfer focus if it was effected by the new column order + // FIXME if the dragged column is partly outside of the view + // port and the focused cell is +-1 of the dragged column, the + // grid scrolls to the right end. maybe fixed when the automatic + // scroll handling is implemented? + final RowContainer rowContainer = escalator + .findRowContainer(focusedCell.getElement()); + if (focusedCellColumnIndex == draggedColumnIndex) { + // move with the dragged column + cellFocusHandler.setCellFocus(focusedRowIndex, + latestColumnDropIndex, rowContainer); + } else if (latestColumnDropIndex <= focusedCellColumnIndex + && draggedColumnIndex > focusedCellColumnIndex) { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedCellColumnIndex + 1, rowContainer); + } else if (latestColumnDropIndex >= focusedCellColumnIndex + && draggedColumnIndex < focusedCellColumnIndex) { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedCellColumnIndex - 1, rowContainer); + } + } + } + @Override public void onDragCancel() { // cancel next click so that we may prevent column sorting if diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java index 0e339ec0ae..e7fbc14d89 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java @@ -15,6 +15,9 @@ */ package com.vaadin.tests.components.grid.basicfeatures; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import java.util.ArrayList; import java.util.List; @@ -132,4 +135,45 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { testUrl = testUrl.replace("?&", "?"); driver.get(testUrl); } + + protected void focusCell(int row, int column) { + getGridElement().getCell(row, column).click(); + } + + protected void assertColumnHeaderOrder(int... indices) { + List headers = getGridHeaderRowCells(); + for (int i = 0; i < indices.length; i++) { + assertColumnHeader("Column " + indices[i], headers.get(i)); + } + } + + protected void assertColumnHeader(String expectedHeaderCaption, + TestBenchElement testBenchElement) { + assertEquals(expectedHeaderCaption.toLowerCase(), testBenchElement + .getText().toLowerCase()); + } + + protected WebElement getDefaultColumnHeader(int index) { + List headerRowCells = getGridHeaderRowCells(); + return headerRowCells.get(index); + } + + protected void dragDefaultColumnHeader(int draggedColumnHeaderIndex, + int onTopOfColumnHeaderIndex, int xOffsetFromColumnTopLeftCorner) { + new Actions(getDriver()) + .clickAndHold(getDefaultColumnHeader(draggedColumnHeaderIndex)) + .moveToElement( + getDefaultColumnHeader(onTopOfColumnHeaderIndex), + xOffsetFromColumnTopLeftCorner, 0).release().perform(); + } + + protected void assertColumnIsSorted(int index) { + WebElement columnHeader = getDefaultColumnHeader(index); + assertTrue(columnHeader.getAttribute("class").contains("sort")); + } + + protected void assertFocusedCell(int row, int column) { + assertTrue(getGridElement().getCell(row, column).getAttribute("class") + .contains("focused")); + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java deleted file mode 100644 index eda064284c..0000000000 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderEventTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2000-2014 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.tests.components.grid.basicfeatures; - -import static org.junit.Assert.assertEquals; - -import org.junit.Before; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.WebElement; - -import com.vaadin.testbench.elements.GridElement.GridCellElement; -import com.vaadin.tests.annotations.TestCategory; - -/** - * - * @since - * @author Vaadin Ltd - */ -@TestCategory("grid") -public class GridColumnReorderEventTest extends GridBasicClientFeaturesTest { - - @Before - public void before() { - openTestURL(); - } - - @Test - public void columnReorderEventTriggered() { - final int firstIndex = 3; - final int secondIndex = 4; - final String firstHeaderText = getGridElement().getHeaderCell(0, - firstIndex).getText(); - final String secondHeaderText = getGridElement().getHeaderCell(0, - secondIndex).getText(); - selectMenuPath("Component", "Internals", "Listeners", - "Add ColumnReorder listener"); - selectMenuPath("Component", "Columns", "Column " + secondIndex, - "Move column left"); - // columns 3 and 4 should have swapped to 4 and 3 - GridCellElement headerCell = getGridElement().getHeaderCell(0, - firstIndex); - assertEquals(secondHeaderText, headerCell.getText()); - headerCell = getGridElement().getHeaderCell(0, secondIndex); - assertEquals(firstHeaderText, headerCell.getText()); - - // the reorder event should have typed the order to this label - WebElement columnReorderElement = findElement(By.id("columnreorder")); - int eventIndex = Integer.parseInt(columnReorderElement - .getAttribute("columns")); - assertEquals(1, eventIndex); - - // trigger another event - selectMenuPath("Component", "Columns", "Column " + secondIndex, - "Move column left"); - columnReorderElement = findElement(By.id("columnreorder")); - eventIndex = Integer.parseInt(columnReorderElement - .getAttribute("columns")); - assertEquals(2, eventIndex); - } -} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java new file mode 100644 index 0000000000..9f43c39b1e --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -0,0 +1,233 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.tests.annotations.TestCategory; + +/** + * + * @since + * @author Vaadin Ltd + */ +@TestCategory("grid") +public class GridColumnReorderTest extends GridBasicClientFeaturesTest { + + @Before + public void before() { + openTestURL(); + } + + @Test + public void columnReorderEventTriggered() { + final int firstIndex = 3; + final int secondIndex = 4; + final String firstHeaderText = getGridElement().getHeaderCell(0, + firstIndex).getText(); + final String secondHeaderText = getGridElement().getHeaderCell(0, + secondIndex).getText(); + selectMenuPath("Component", "Internals", "Listeners", + "Add ColumnReorder listener"); + selectMenuPath("Component", "Columns", "Column " + secondIndex, + "Move column left"); + // columns 3 and 4 should have swapped to 4 and 3 + GridCellElement headerCell = getGridElement().getHeaderCell(0, + firstIndex); + assertEquals(secondHeaderText, headerCell.getText()); + headerCell = getGridElement().getHeaderCell(0, secondIndex); + assertEquals(firstHeaderText, headerCell.getText()); + + // the reorder event should have typed the order to this label + WebElement columnReorderElement = findElement(By.id("columnreorder")); + int eventIndex = Integer.parseInt(columnReorderElement + .getAttribute("columns")); + assertEquals(1, eventIndex); + + // trigger another event + selectMenuPath("Component", "Columns", "Column " + secondIndex, + "Move column left"); + columnReorderElement = findElement(By.id("columnreorder")); + eventIndex = Integer.parseInt(columnReorderElement + .getAttribute("columns")); + assertEquals(2, eventIndex); + } + + @Test + public void testColumnReorder_onReorder_columnReorderEventTriggered() { + final int firstIndex = 3; + final int secondIndex = 4; + final String firstHeaderText = getGridElement().getHeaderCell(0, + firstIndex).getText(); + final String secondHeaderText = getGridElement().getHeaderCell(0, + secondIndex).getText(); + selectMenuPath("Component", "Internals", "Listeners", + "Add ColumnReorder listener"); + selectMenuPath("Component", "Columns", "Column " + secondIndex, + "Move column left"); + // columns 3 and 4 should have swapped to 4 and 3 + GridCellElement headerCell = getGridElement().getHeaderCell(0, + firstIndex); + assertEquals(secondHeaderText, headerCell.getText()); + headerCell = getGridElement().getHeaderCell(0, secondIndex); + assertEquals(firstHeaderText, headerCell.getText()); + + // the reorder event should have typed the order to this label + WebElement columnReorderElement = findElement(By.id("columnreorder")); + int eventIndex = Integer.parseInt(columnReorderElement + .getAttribute("columns")); + assertEquals(1, eventIndex); + + // trigger another event + selectMenuPath("Component", "Columns", "Column " + secondIndex, + "Move column left"); + columnReorderElement = findElement(By.id("columnreorder")); + eventIndex = Integer.parseInt(columnReorderElement + .getAttribute("columns")); + assertEquals(2, eventIndex); + } + + @Test + public void testColumnReorder_draggingSortedColumn_sortIndicatorShownOnDraggedElement() { + // given + toggleColumnReorder(); + toggleSortableColumn(0); + sortColumn(0); + + // when + startDragButDontDropOnDefaultColumnHeader(0); + + // then + WebElement draggedElement = getDraggedHeaderElement(); + assertTrue(draggedElement.getAttribute("class").contains("sort")); + } + + @Test + public void testColumnReorder_draggingSortedColumn_sortStays() { + // given + toggleColumnReorder(); + toggleSortableColumn(0); + sortColumn(0); + + // when + dragDefaultColumnHeader(0, 2, 10); + + // then + assertColumnIsSorted(1); + } + + @Test + public void testColumnReorder_draggingFocusedHeader_focusShownOnDraggedElement() { + // given + toggleColumnReorder(); + focusDefaultHeader(0); + + // when + startDragButDontDropOnDefaultColumnHeader(0); + + // then + WebElement draggedElement = getDraggedHeaderElement(); + assertTrue(draggedElement.getAttribute("class").contains("focused")); + } + + @Test + public void testColumnReorder_draggingFocusedHeader_focusIsKeptOnHeader() { + // given + toggleColumnReorder(); + focusDefaultHeader(0); + + // when + dragDefaultColumnHeader(0, 3, 10); + + // then + WebElement defaultColumnHeader = getDefaultColumnHeader(2); + String attribute = defaultColumnHeader.getAttribute("class"); + assertTrue(attribute.contains("focused")); + } + + @Test + public void testColumnReorder_draggingFocusedCellColumn_focusIsKeptOnCell() { + // given + toggleColumnReorder(); + focusCell(2, 2); + + // when + dragDefaultColumnHeader(2, 0, 10); + + // then + assertFocusedCell(2, 0); + } + + @Test + public void testColumnReorder_dragColumnFromRightToLeftOfFocusedCellColumn_focusIsKept() { + // given + toggleColumnReorder(); + focusCell(1, 3); + + // when + dragDefaultColumnHeader(4, 1, 10); + + // then + assertFocusedCell(1, 4); + } + + @Test + public void testColumnReorder_dragColumnFromLeftToRightOfFocusedCellColumn_focusIsKept() { + // given + toggleColumnReorder(); + focusCell(4, 2); + + // when + dragDefaultColumnHeader(0, 4, 10); + + // then + assertFocusedCell(4, 1); + } + + private void toggleColumnReorder() { + selectMenuPath("Component", "State", "Column Reordering"); + } + + private void toggleSortableColumn(int index) { + selectMenuPath("Component", "Columns", "Column " + index, "Sortable"); + } + + private void startDragButDontDropOnDefaultColumnHeader(int index) { + new Actions(getDriver()) + .clickAndHold(getGridHeaderRowCells().get(index)) + .moveByOffset(100, 0).perform(); + } + + private void sortColumn(int index) { + getGridHeaderRowCells().get(index).click(); + } + + private void focusDefaultHeader(int index) { + getGridHeaderRowCells().get(index).click(); + } + + private WebElement getDraggedHeaderElement() { + return findElement(By.className("dragged-column-header")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java index 2f00071351..2cc8610209 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java @@ -15,18 +15,12 @@ */ package com.vaadin.tests.components.grid.basicfeatures.server; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import java.util.List; - import org.junit.Before; import org.junit.Test; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.interactions.Actions; -import com.vaadin.testbench.TestBenchElement; import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; /** @@ -212,30 +206,4 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertFalse(logRow.contains("Columns reordered")); } - private void assertColumnHeaderOrder(int... indices) { - List headers = getGridHeaderRowCells(); - for (int i = 0; i < indices.length; i++) { - assertColumnHeader("Column " + indices[i], headers.get(i)); - } - } - - private void assertColumnHeader(String expectedHeaderCaption, - TestBenchElement testBenchElement) { - assertEquals(expectedHeaderCaption.toLowerCase(), testBenchElement - .getText().toLowerCase()); - } - - private WebElement getDefaultColumnHeader(int index) { - List headerRowCells = getGridHeaderRowCells(); - return headerRowCells.get(index); - } - - private void dragDefaultColumnHeader(int draggedColumnHeaderIndex, - int onTopOfColumnHeaderIndex, int xOffsetFromColumnTopLeftCorner) { - new Actions(getDriver()) - .clickAndHold(getDefaultColumnHeader(draggedColumnHeaderIndex)) - .moveToElement( - getDefaultColumnHeader(onTopOfColumnHeaderIndex), - xOffsetFromColumnTopLeftCorner, 0).release().perform(); - } } -- cgit v1.2.3 From 7d65a56a9b91f37da079dc15d678cff115cd4d46 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Thu, 19 Feb 2015 14:53:35 +0200 Subject: Fixes Escalator row offsets with spacers open (#16644) Caveat: Scrolling works, as long as it is done slowly, one-by-one. When moving several rows in a batch, spacers are ignored and rows end up underneath them. Change-Id: I07d0135e4ac559f5553cd8dc85bca39061de69b7 --- .../src/com/vaadin/client/widgets/Escalator.java | 284 +++++++++++++++++---- 1 file changed, 237 insertions(+), 47 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 1d3faee51b..5711ca4731 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -2406,10 +2406,23 @@ public class Escalator extends Widget implements RequiresResize, 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 @@ -2419,14 +2432,17 @@ public class Escalator extends Widget implements RequiresResize, if (viewportOffset > 0) { // there's empty room on top - int originalRowsToMove = (int) Math.ceil(viewportOffset + double rowPx = getRowHeightsSumBetweenPx(scrollTop, + topElementPosition); + int originalRowsToMove = (int) Math.ceil(rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, visualRowOrder.size()); final int end = visualRowOrder.size(); final int start = end - rowsToMove; - final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + final int logicalRowIndex = getLogicalRowIndex(scrollTop); + moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); @@ -2435,14 +2451,16 @@ public class Escalator extends Widget implements RequiresResize, rowsWereMoved = true; } - else if (viewportOffset + getDefaultRowHeight() <= 0) { + 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, visualRowOrder.size()); @@ -2460,7 +2478,7 @@ public class Escalator extends Widget implements RequiresResize, * calculate the first logical row index from the scroll * position. */ - logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); + logicalRowIndex = getLogicalRowIndex(scrollTop); } /* @@ -2518,9 +2536,6 @@ public class Escalator extends Widget implements RequiresResize, } if (rowsWereMoved) { - // TODO - getLogger().warning("[[spacers]] rotating rows during scroll"); - fireRowVisibilityChangeEvent(); if (scroller.touchHandlerBundle.touches == 0) { @@ -2534,6 +2549,27 @@ public class Escalator extends Widget implements RequiresResize, } } + 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, + SpacerMeasurementStrategy.PARTIAL, y2, + SpacerMeasurementStrategy.PARTIAL); + + return viewportPx - spacerPx; + } + + private int getLogicalRowIndex(final double px) { + /* + * FIXME: this is buggy! if px is mid-spacer, it will return the + * pixel count up until PX, not the one after (or before) it. + */ + double rowPx = px + - spacerContainer.getSpacerCompleteHeightsSumUntilPx(px); + return (int) (rowPx / getDefaultRowHeight()); + } + @Override protected void paintInsertRows(final int index, final int numberOfRows) { if (numberOfRows == 0) { @@ -2632,12 +2668,6 @@ public class Escalator extends Widget implements RequiresResize, * 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 visualSourceRange.getStart(), - * visualTargetIndex or - * logicalTargetIndex is a negative number; or - * if visualTargetInfo is greater than the - * number of escalator rows. */ private void moveAndUpdateEscalatorRows(final Range visualSourceRange, final int visualTargetIndex, final int logicalTargetIndex) @@ -2647,28 +2677,24 @@ public class Escalator extends Widget implements RequiresResize, 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"; + + assert visualTargetIndex >= 0 : "Visual target must be 0 or greater"; + + assert visualTargetIndex <= root.getChildCount() : "Visual target " + + "must not be greater than the number of escalator rows"; + + 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 @@ -2723,7 +2749,7 @@ public class Escalator extends Widget implements RequiresResize, } { // Reposition the rows that were moved - double newRowTop = logicalTargetIndex * getDefaultRowHeight(); + double newRowTop = getRowTop(logicalTargetIndex); final ListIterator iter = visualRowOrder .listIterator(adjustedVisualTargetIndex); @@ -3688,8 +3714,9 @@ public class Escalator extends Widget implements RequiresResize, return spacerContainer.getSpacerUpdater(); } - private double calculateRowTop(int logicalIndex) { - double top = spacerContainer.getSpacerHeightsSumUntil(logicalIndex); + private double getRowTop(int logicalIndex) { + double top = spacerContainer + .getSpacerHeightsSumUntilIndex(logicalIndex); return top + (logicalIndex * getDefaultRowHeight()); } @@ -3701,7 +3728,8 @@ public class Escalator extends Widget implements RequiresResize, if (row > getTopRowLogicalIndex()) { // TODO getLogger().warning( - "scrollbar compensation not yet implemented"); + "[[spacers]] scrollbar compensation not yet " + + "implemented"); } } @@ -4200,6 +4228,21 @@ public class Escalator extends Widget implements RequiresResize, } } + /** + * A decision on how to measure a spacer when it is partially within a + * designated range. + */ + public enum SpacerMeasurementStrategy { + /** Take the entire spacer's height into account. */ + COMPLETE, + + /** Take the visible part into account. */ + PARTIAL, + + /** Exclude the entire spacer. */ + NONE + } + private class SpacerContainer { /** This is used mainly for testing purposes */ @@ -4283,10 +4326,12 @@ public class Escalator extends Widget implements RequiresResize, } private final TreeMap rowIndexToSpacer = new TreeMap(); + private SpacerUpdater spacerUpdater = SpacerUpdater.NULL; public void setSpacer(int rowIndex, double height) throws IllegalArgumentException { + if (rowIndex < 0 || rowIndex >= getBody().getRowCount()) { throw new IllegalArgumentException("invalid row index: " + rowIndex + ", while the body only has " @@ -4304,8 +4349,154 @@ public class Escalator extends Widget implements RequiresResize, } } + /** + * Gets the amount of pixels occupied by spacers between two pixel + * points. + * + * @param rangeTop + * the top pixel point + * @param topInclusion + * how to measure a spacer that happens to lie in the middle + * of {@code rangeTop}. + * @param rangeBottom + * the bottom pixel point + * @param bottomInclusion + * how to measure a spacer that happens to lie in the middle + * of {@code rangeBottom}. + * @return the pixels occupied by spacers between {@code rangeTop} and + * {@code rangeBottom} + */ + public double getSpacerHeightsSumBetweenPx(double rangeTop, + SpacerMeasurementStrategy topInclusion, double rangeBottom, + SpacerMeasurementStrategy 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. + *

+ * If a spacer lies in the middle of {@code px}, then the entire height + * of the spacer will be taken into account. + * + * @param px + * pixels counted from the top + * @return the pixels occupied by entire spacers up until {@code px} + */ + public double getSpacerCompleteHeightsSumUntilPx(double px) { + return getSpacerHeightsSumBetweenPx(0, + SpacerMeasurementStrategy.COMPLETE, px, + SpacerMeasurementStrategy.COMPLETE); + } + + /** + * 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 getSpacerHeightsSumUntil(int logicalIndex) { + public double getSpacerHeightsSumUntilIndex(int logicalIndex) { double heights = 0; for (SpacerImpl spacer : rowIndexToSpacer.headMap(logicalIndex, false).values()) { @@ -4339,7 +4530,7 @@ public class Escalator extends Widget implements RequiresResize, getSpacer(rowIndex).setHeight(newHeight); } - private SpacerImpl getSpacer(int rowIndex) { + public SpacerImpl getSpacer(int rowIndex) { return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); } @@ -4425,8 +4616,7 @@ public class Escalator extends Widget implements RequiresResize, } private double calculateSpacerTop(int logicalIndex) { - return body.calculateRowTop(logicalIndex) - + body.getDefaultRowHeight(); + return body.getRowTop(logicalIndex) + body.getDefaultRowHeight(); } @SuppressWarnings("boxing") -- cgit v1.2.3 From 2b8846335d5e315966e2698faafe8f8695198f66 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 23 Feb 2015 13:45:44 +0200 Subject: Fixes fast Escalator scrolling with spacers (#16644). Change-Id: I2c79cc972e35ffe37a6fb3cc97b6fc937c3d32f7 --- client/src/com/vaadin/client/widgets/Escalator.java | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 5711ca4731..bca0cc9afb 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -2756,7 +2756,13 @@ public class Escalator extends Widget implements RequiresResize, for (int i = 0; i < visualSourceRange.length(); i++) { final TableRowElement tr = iter.next(); setRowPosition(tr, 0, newRowTop); + newRowTop += getDefaultRowHeight(); + SpacerContainer.SpacerImpl spacer = spacerContainer + .getSpacer(logicalTargetIndex + i); + if (spacer != null) { + newRowTop += spacer.getHeight(); + } } } } -- cgit v1.2.3 From d2706131010e19e3fe5efceab118592efb18cf77 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 23 Feb 2015 17:31:13 +0200 Subject: Spacers affect Escalator's scrollbars (#16644) Change-Id: I73a7a6db4d25c09d4a3eb04c4aea18f06428d320 --- .../src/com/vaadin/client/widgets/Escalator.java | 145 ++++++++++++++++----- 1 file changed, 115 insertions(+), 30 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index bca0cc9afb..185744d2ec 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -1144,6 +1144,9 @@ public class Escalator extends Widget implements RequiresResize, public void scrollToRow(final int rowIndex, final ScrollDestination destination, final double padding) { + + getLogger().warning("[[spacers]] scrollToRow"); + final double targetStartPx = body.getDefaultRowHeight() * rowIndex; final double targetEndPx = targetStartPx + body.getDefaultRowHeight(); @@ -2317,7 +2320,7 @@ public class Escalator extends Widget implements RequiresResize, this.topRowLogicalIndex = topRowLogicalIndex; } - private int getTopRowLogicalIndex() { + public int getTopRowLogicalIndex() { return topRowLogicalIndex; } @@ -2610,7 +2613,7 @@ public class Escalator extends Widget implements RequiresResize, */ final double yDelta = numberOfRows * getDefaultRowHeight(); - adjustScrollPosIgnoreEvents(yDelta); + moveViewportAndContent(yDelta); updateTopRowLogicalIndex(numberOfRows); } @@ -2768,30 +2771,53 @@ public class Escalator extends Widget implements RequiresResize, } /** - * Adjust the scroll position without having the scroll handler have any - * side-effects. + * Adjust the scroll position and move the contained rows. + *

+ * Note: This method does not account for spacers. *

- * Note: {@link Scroller#onScroll()} will 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 with it" and renders them appropriately. + * The viewport may be scrolled any arbitrary amount, and the rows are + * moved appropriately, but always snapped into a plausible place. + *

+ *

+ *
Example 1
+ *
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.
+ *
Example 2
+ *
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.
+ *
* * @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) { + + /* + * TODO: When adding and removing rows starts supporting spacers, + * this method should also take spacers into account. Remember to + * adjust the javadoc as well. + */ + if (yDelta == 0) { return; } - verticalScrollbar.setScrollPosByDelta(yDelta); + double newTop = tBodyScrollTop + yDelta; + final double rowTopPos = body.getRowTop(getLogicalRowIndex(newTop)); + + verticalScrollbar.setScrollPos(newTop); - final double rowTopPos = yDelta - (yDelta % getDefaultRowHeight()); - for (final TableRowElement tr : visualRowOrder) { - setRowPosition(tr, 0, getRowTop(tr) + rowTopPos); + for (int i = 0; i < visualRowOrder.size(); i++) { + final TableRowElement tr = visualRowOrder.get(i); + setRowPosition(tr, 0, rowTopPos + getDefaultRowHeight() * i); } - setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta); + setBodyScrollPosition(tBodyScrollLeft, newTop); } /** @@ -2915,7 +2941,7 @@ public class Escalator extends Widget implements RequiresResize, * 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 @@ -2924,8 +2950,7 @@ public class Escalator extends Widget implements RequiresResize, * current negative scrolltop, presto!), so that it isn't * aligned funnily */ - adjustScrollPosIgnoreEvents(-verticalScrollbar - .getScrollPos()); + moveViewportAndContent(-verticalScrollbar.getScrollPos()); } } @@ -3730,13 +3755,6 @@ public class Escalator extends Widget implements RequiresResize, for (TableRowElement tr : getVisibleRowsAfter(row)) { setRowPosition(tr, 0, getRowTop(tr) + diff); } - - if (row > getTopRowLogicalIndex()) { - // TODO - getLogger().warning( - "[[spacers]] scrollbar compensation not yet " - + "implemented"); - } } private List getVisibleRowsAfter(int logicalRow) { @@ -4302,13 +4320,80 @@ public class Escalator extends Widget implements RequiresResize, assert height >= 0 : "Height must be more >= 0 (was " + height + ")"; - double diff = height - Math.max(0, this.height); + final double heightDiff = height - Math.max(0, this.height); + final double oldHeight = this.height; + this.height = height; root.getStyle().setHeight(height, Unit.PX); - shiftSpacerPositions(getRow(), diff); - body.shiftRowPositions(getRow(), diff); - body.verifyEscalatorCount(); + // move the visible spacers getRow row onwards. + shiftSpacerPositions(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); + } + + boolean viewportNeedsScrolling = getRow() < body + .getTopRowLogicalIndex(); + 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); + } } @Override -- cgit v1.2.3 From 6db686cd52e2c72387f555523eedf4f81708d0ab Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Tue, 24 Feb 2015 10:56:00 +0200 Subject: Auto scroll when dnd reorder columns and cursor close to Grid edge. (#16643) Change-Id: I38c18d44afb6666677c1593541021d36c8cb2afc --- .../vaadin/client/ui/dd/DragAndDropHandler.java | 45 +- .../vaadin/client/widget/grid/AutoScroller.java | 649 +++++++++++++++++++++ client/src/com/vaadin/client/widgets/Grid.java | 53 +- 3 files changed, 720 insertions(+), 27 deletions(-) create mode 100644 client/src/com/vaadin/client/widget/grid/AutoScroller.java (limited to 'client') diff --git a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java index c8b651bd4e..eec52d5def 100644 --- a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -47,8 +47,11 @@ public class DragAndDropHandler { public interface DragAndDropCallback { /** * Called when the drag has started. + * + * @param startEvent + * the original event that started the drag */ - void showDragElement(); + void onDragStart(NativeEvent startEvent); /** * Called on drag. @@ -56,12 +59,12 @@ public class DragAndDropHandler { * @param event * the event related to the drag */ - void updateDragElement(NativePreviewEvent event); + void onDragUpdate(NativePreviewEvent event); /** - * Called when the has ended on a drop or cancel. + * Called after the has ended on a drop or cancel. */ - void removeDragElement(); + void onDragEnd(); /** * Called when the drag has ended on a drop. @@ -69,7 +72,7 @@ public class DragAndDropHandler { void onDrop(); /** - * Called when the drag has been canceled before drop. + * Called when the drag has been canceled. */ void onDragCancel(); } @@ -97,7 +100,7 @@ public class DragAndDropHandler { break; case Event.ONMOUSEMOVE: case Event.ONTOUCHMOVE: - callback.updateDragElement(event); + callback.onDragUpdate(event); // prevent text selection on IE event.getNativeEvent().preventDefault(); break; @@ -109,7 +112,7 @@ public class DragAndDropHandler { event.getNativeEvent().preventDefault(); //$FALL-THROUGH$ case Event.ONMOUSEUP: - callback.updateDragElement(event); + callback.onDragUpdate(event); callback.onDrop(); stopDrag(); event.cancel(); @@ -122,14 +125,6 @@ public class DragAndDropHandler { } } - private void cancelDrag(NativePreviewEvent event) { - callback.onDragCancel(); - callback.removeDragElement(); - stopDrag(); - event.cancel(); - event.getNativeEvent().preventDefault(); - } - }; private static Logger getLogger() { @@ -194,7 +189,7 @@ public class DragAndDropHandler { if (Math.abs(startX - currentX) > 3 || Math.abs(startY - currentY) > 3) { removeNativePreviewHandlerRegistration(); - startDrag(event, callback); + startDrag(dragStartingEvent, event, callback); } break; default: @@ -207,15 +202,15 @@ public class DragAndDropHandler { }); } - private void startDrag(NativePreviewEvent event, - DragAndDropCallback callback) { + private void startDrag(NativeEvent startEvent, + NativePreviewEvent triggerEvent, DragAndDropCallback callback) { dragging = true; // just capture something to prevent text selection in IE Event.setCapture(RootPanel.getBodyElement()); this.callback = callback; dragHandlerRegistration = Event.addNativePreviewHandler(dragHandler); - callback.showDragElement(); - callback.updateDragElement(event); + callback.onDragStart(startEvent); + callback.onDragUpdate(triggerEvent); } private void stopDrag() { @@ -226,11 +221,19 @@ public class DragAndDropHandler { } Event.releaseCapture(RootPanel.getBodyElement()); if (callback != null) { - callback.removeDragElement(); + 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(); 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..8f4e2036fc --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -0,0 +1,649 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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.BrowserEvents; +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 + * @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. + * + * @since + */ + public interface AutoScrollerCallback { + + /** + * Triggered when doing automatic horizontal scrolling. + * + * @param scrollDiff + * the amount of pixels that have been auto scrolled since + * last call + */ + void onHorizontalAutoScroll(int scrollDiff); + + /** + * Triggered when doing automatic vertical scrolling. + * + * @param scrollDiff + * the amount of pixels that have been auto scrolled since + * last call + */ + void onVerticalAutoScroll(int scrollDiff); + } + + 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. + *

+ * Techical note: 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) { + if (scrollDirection == ScrollAxis.VERTICAL) { + grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll); + callback.onVerticalAutoScroll(intPixelsToScroll); + } else { + grid.setScrollLeft(grid.getScrollLeft() + intPixelsToScroll); + callback.onHorizontalAutoScroll(intPixelsToScroll); + } + } + + 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. + *

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

+ * 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. The given event must be a touch + * start or a left mouse triggered mouse down event. + * + * @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) { + if (BrowserEvents.TOUCHSTART.equals(startEvent.getType()) + || (BrowserEvents.MOUSEDOWN.equals(startEvent.getType()) && startEvent + .getButton() == NativeEvent.BUTTON_LEFT)) { + scrollDirection = scrollAxis; + this.callback = callback; + injectNativeHandler(); + start(startEvent); + startEvent.preventDefault(); + startEvent.stopPropagation(); + } else { + throw new IllegalStateException("received unexpected event: " + + startEvent.getType()); + } + } + + /** + * 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. + *

+ * Defaults to 100px. + * + * @param px + * the height/width for the auto scroll area depending on + * direction + */ + public void setScrollAreaPX(int px) { + scrollAreaPX = px; + } + + /** + * Returns the size of the auto scroll area in pixels. + *

+ * Defaults to 100px. + * + * @return size in pixels + */ + public int getScrollAreaPX() { + return scrollAreaPX; + } + + private void start(final NativeEvent event) { + /* + * 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(); + + for (int i = 0; i < grid.getFrozenColumnCount(); i++) { + startBorder += grid.getColumn(i).getWidthActual(); + } + + 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()); + } + } +} diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index d54c023e3d..514cb6185c 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -83,6 +83,9 @@ 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.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; @@ -2865,8 +2868,22 @@ public class Grid extends ResizeComposite implements private DragAndDropHandler dndHandler = new DragAndDropHandler(); + private AutoScroller autoScroller = new AutoScroller(this); + private DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() { + private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { + + @Override + public void onVerticalAutoScroll(int scrollDiff) { + // NOP + } + + @Override + public void onHorizontalAutoScroll(int scrollDiff) { + onDragUpdate(null); + } + }; /** * Elements for displaying the dragged column(s) and drop marker * properly @@ -2884,6 +2901,8 @@ public class Grid extends ResizeComposite implements */ private HandlerRegistration columnSortPreventRegistration; + private int clientX; + private void initHeaderDragElementDOM() { if (table == null) { tableHeader = DOM.createTHead(); @@ -2902,9 +2921,11 @@ public class Grid extends ResizeComposite implements } @Override - public void updateDragElement(NativePreviewEvent event) { - int clientX = WidgetUtil.getTouchOrMouseClientX(event - .getNativeEvent()); + public void onDragUpdate(NativePreviewEvent event) { + if (event != null) { + clientX = WidgetUtil.getTouchOrMouseClientX(event + .getNativeEvent()); + } resolveDragElementHorizontalPosition(clientX); updateDragDropMarker(clientX); } @@ -2940,7 +2961,7 @@ public class Grid extends ResizeComposite implements } @Override - public void showDragElement() { + public void onDragStart(NativeEvent startingEvent) { initHeaderDragElementDOM(); // needs to clone focus and sorting indicators too (UX) dragElement = DOM.clone(eventCell.getElement(), true); @@ -2952,10 +2973,14 @@ public class Grid extends ResizeComposite implements eventCell.getElement().addClassName("dragged"); // mark the floating cell, for styling & testing dragElement.addClassName("dragged-column-header"); + // start the auto scroll handler + autoScroller.setScrollAreaPX(60); + autoScroller.start(startingEvent, ScrollAxis.HORIZONTAL, + autoScrollerCallback); } @Override - public void removeDragElement() { + public void onDragEnd() { table.removeFromParent(); dragElement.removeFromParent(); eventCell.getElement().removeClassName("dragged"); @@ -2977,8 +3002,8 @@ public class Grid extends ResizeComposite implements @SuppressWarnings("unchecked") Column[] array = reordered.toArray(new Column[reordered .size()]); - transferCellFocusOnDrop(); setColumnOrder(array); + transferCellFocusOnDrop(); } // else no reordering } @@ -3007,6 +3032,9 @@ public class Grid extends ResizeComposite implements && draggedColumnIndex < focusedCellColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedCellColumnIndex - 1, rowContainer); + } else { + cellFocusHandler.setCellFocus(focusedRowIndex, + focusedCellColumnIndex, rowContainer); } } } @@ -3032,7 +3060,9 @@ public class Grid extends ResizeComposite implements } }); } + autoScroller.stop(); } + }; /** @@ -4972,6 +5002,17 @@ public class Grid extends ResizeComposite implements return escalator.getScrollTop(); } + /** + * Sets the horizontal scroll offset + * + * @since + * @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 * -- cgit v1.2.3 From e88d99ca21c25067960836d520dda8b96f23b04d Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Wed, 25 Feb 2015 09:34:36 +0200 Subject: Fixes a bug with scrolling the Escalator with a spacer open (#16644) Mostly cleanup/clarity enhancements, the actual bug fix is in getSpacerHeightsSumUntilPx Change-Id: Ie9aafc0398e4ed293067c9aeac6281abc0c1df82 --- .../src/com/vaadin/client/widgets/Escalator.java | 52 +++++++++++++++------- 1 file changed, 37 insertions(+), 15 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 185744d2ec..dcc6c98960 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -1913,6 +1913,18 @@ public class Escalator extends Widget implements RequiresResize, positions.set(tr, x, y); } + /** + * Returns the assigned top position for the given element. + *

+ * Note: 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 positions.getTop(tr); } @@ -2564,12 +2576,8 @@ public class Escalator extends Widget implements RequiresResize, } private int getLogicalRowIndex(final double px) { - /* - * FIXME: this is buggy! if px is mid-spacer, it will return the - * pixel count up until PX, not the one after (or before) it. - */ double rowPx = px - - spacerContainer.getSpacerCompleteHeightsSumUntilPx(px); + - spacerContainer.getSpacerHeightsSumUntilPx(px); return (int) (rowPx / getDefaultRowHeight()); } @@ -2685,12 +2693,15 @@ public class Escalator extends Widget implements RequiresResize, + visualSourceRange.getStart() + ")"; assert logicalTargetIndex >= 0 : "Logical target must be 0 or " - + "greater"; + + "greater (was " + logicalTargetIndex + ")"; - assert visualTargetIndex >= 0 : "Visual target must be 0 or greater"; + assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was " + + visualTargetIndex + ")"; assert visualTargetIndex <= root.getChildCount() : "Visual target " - + "must not be greater than the number of escalator rows"; + + "must not be greater than the number of escalator rows (was " + + visualTargetIndex + ", escalator rows " + + root.getChildCount() + ")"; assert logicalTargetIndex + visualSourceRange.length() <= getRowCount() : "Logical " + "target leads to rows outside of the data range (" @@ -3745,6 +3756,20 @@ public class Escalator extends Widget implements RequiresResize, return spacerContainer.getSpacerUpdater(); } + /** + * Calculates the correct top position of a row at a logical + * index, regardless if there is one there or not. + *

+ * 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); @@ -4564,18 +4589,15 @@ public class Escalator extends Widget implements RequiresResize, /** * Gets the amount of pixels occupied by spacers from the top until a * certain spot from the top of the body. - *

- * If a spacer lies in the middle of {@code px}, then the entire height - * of the spacer will be taken into account. * * @param px * pixels counted from the top - * @return the pixels occupied by entire spacers up until {@code px} + * @return the pixels occupied by spacers up until {@code px} */ - public double getSpacerCompleteHeightsSumUntilPx(double px) { + public double getSpacerHeightsSumUntilPx(double px) { return getSpacerHeightsSumBetweenPx(0, - SpacerMeasurementStrategy.COMPLETE, px, - SpacerMeasurementStrategy.COMPLETE); + SpacerMeasurementStrategy.PARTIAL, px, + SpacerMeasurementStrategy.PARTIAL); } /** -- cgit v1.2.3 From c2fdfcab42f663e46d8f5bff84aa7ca90f7a531e Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Wed, 25 Feb 2015 09:22:06 +0200 Subject: Adjust column reorder drop marker when auto scrolling grid. (#16643) Change-Id: I11377fb8b007c3c50c3e3bd1c29afcf8485431f0 --- .../vaadin/client/widget/grid/AutoScroller.java | 75 ++++++++++++++++------ .../src/com/vaadin/client/widgets/Escalator.java | 22 +++++++ client/src/com/vaadin/client/widgets/Grid.java | 50 +++++++++++---- 3 files changed, 118 insertions(+), 29 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/grid/AutoScroller.java b/client/src/com/vaadin/client/widget/grid/AutoScroller.java index 8f4e2036fc..60c9535ca7 100644 --- a/client/src/com/vaadin/client/widget/grid/AutoScroller.java +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -43,28 +43,34 @@ public class AutoScroller { /** * Callback that notifies when the cursor is on top of a new row or column * because of the automatic scrolling. - * - * @since */ public interface AutoScrollerCallback { /** - * Triggered when doing automatic horizontal scrolling. + * Triggered when doing automatic scrolling. + *

+ * 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 onHorizontalAutoScroll(int scrollDiff); + void onAutoScroll(int scrollDiff); /** - * Triggered when doing automatic vertical scrolling. - * - * @param scrollDiff - * the amount of pixels that have been auto scrolled since - * last call + * Triggered when the grid scroll has reached the minimum scroll + * position. Depending on the scroll axis, either scrollLeft or + * scrollTop is 0. */ - void onVerticalAutoScroll(int scrollDiff); + 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 { @@ -229,12 +235,30 @@ public class AutoScroller { pixelsToScroll -= intPixelsToScroll; if (intPixelsToScroll != 0) { + double scrollPos; + double maxScrollPos; + double newScrollPos; if (scrollDirection == ScrollAxis.VERTICAL) { - grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll); - callback.onVerticalAutoScroll(intPixelsToScroll); + scrollPos = grid.getScrollTop(); + maxScrollPos = getMaxScrollTop(); } else { - grid.setScrollLeft(grid.getScrollLeft() + intPixelsToScroll); - callback.onHorizontalAutoScroll(intPixelsToScroll); + 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(); + } } } @@ -521,10 +545,7 @@ public class AutoScroller { private void updateScrollBounds() { double startBorder = getBodyClientStart(); final int endBorder = getBodyClientEnd(); - - for (int i = 0; i < grid.getFrozenColumnCount(); i++) { - startBorder += grid.getColumn(i).getWidthActual(); - } + startBorder += getFrozenColumnsWidth(); final int scrollCompensation = getScrollCompensation(); startingBound = scrollCompensation + startBorder + scrollAreaPX; @@ -646,4 +667,22 @@ public class AutoScroller { return getClientLeft(getTbodyElement()); } } + + private double getFrozenColumnsWidth() { + double value = 0; + for (int i = 0; i < grid.getFrozenColumnCount(); i++) { + value += grid.getColumn(i).getWidthActual(); + } + return value; + } + + 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/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index ca54b97ca5..c9a1efde98 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -4713,6 +4713,28 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker horizontalScrollbar.setScrollPos(scrollLeft); } + /** + * Returns the scroll width for the escalator. Note that this is not + * necessary the same as {@code Element.scrollWidth} in the DOM. + * + * @since + * @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 + * @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 diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 514cb6185c..1d3f090a62 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -2875,13 +2875,23 @@ public class Grid extends ResizeComposite implements private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { @Override - public void onVerticalAutoScroll(int scrollDiff) { - // NOP + public void onAutoScroll(int scrollDiff) { + autoScrollX = scrollDiff; + onDragUpdate(null); } @Override - public void onHorizontalAutoScroll(int scrollDiff) { - onDragUpdate(null); + 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); } }; /** @@ -2903,6 +2913,9 @@ public class Grid extends ResizeComposite implements private int clientX; + /** How much the grid is being auto scrolled while dragging. */ + private int autoScrollX; + private void initHeaderDragElementDOM() { if (table == null) { tableHeader = DOM.createTHead(); @@ -2925,6 +2938,7 @@ public class Grid extends ResizeComposite implements if (event != null) { clientX = WidgetUtil.getTouchOrMouseClientX(event .getNativeEvent()); + autoScrollX = 0; } resolveDragElementHorizontalPosition(clientX); updateDragDropMarker(clientX); @@ -2946,6 +2960,7 @@ public class Grid extends ResizeComposite implements dropMarkerLeft += cellWidth; } } + dropMarkerLeft += autoScrollX; if (dropMarkerLeft > header.getElement().getOffsetWidth() || dropMarkerLeft < 0) { dropMarkerLeft = -10000000; @@ -3014,10 +3029,6 @@ public class Grid extends ResizeComposite implements final int focusedRowIndex = focusedCell.getRow(); final int draggedColumnIndex = eventCell.getColumnIndex(); // transfer focus if it was effected by the new column order - // FIXME if the dragged column is partly outside of the view - // port and the focused cell is +-1 of the dragged column, the - // grid scrolls to the right end. maybe fixed when the automatic - // scroll handling is implemented? final RowContainer rowContainer = escalator .findRowContainer(focusedCell.getElement()); if (focusedCellColumnIndex == draggedColumnIndex) { @@ -3032,9 +3043,6 @@ public class Grid extends ResizeComposite implements && draggedColumnIndex < focusedCellColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedCellColumnIndex - 1, rowContainer); - } else { - cellFocusHandler.setCellFocus(focusedRowIndex, - focusedCellColumnIndex, rowContainer); } } } @@ -5022,6 +5030,26 @@ public class Grid extends ResizeComposite implements return escalator.getScrollLeft(); } + /** + * Returns the height of the scrollable area in pixels. + * + * @since + * @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 + * @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()); } -- cgit v1.2.3 From b90e4d307c286a56a38fc3cc2ec2dd73017e9458 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Wed, 25 Feb 2015 16:16:10 +0200 Subject: Column reordering in Grid restricted to non-frozen columns. (#16643) Can't drag frozen columns. Dragging no top of frozen column will keep drag element and drop marker on "right". Dropping on frozen column will drop the column to the right of the frozen columns. Change-Id: Ib04af96b21072e9f19dc7640a67bc68d49a9ae54 --- client/src/com/vaadin/client/widgets/Grid.java | 23 +++++++++- .../grid/basicfeatures/GridBasicFeaturesTest.java | 7 ++- .../grid/basicfeatures/GridColumnReorderTest.java | 10 ++--- .../server/GridColumnReorderTest.java | 52 +++++++++++++++++----- 4 files changed, 74 insertions(+), 18 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 1d3f090a62..fc208843e4 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -2961,7 +2961,16 @@ public class Grid extends ResizeComposite implements } } dropMarkerLeft += autoScrollX; - if (dropMarkerLeft > header.getElement().getOffsetWidth() + + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (dropMarkerLeft < frozenColumnsWidth) { + latestColumnDropIndex = getFrozenColumnCount(); + if (getScrollLeft() == 0) { + dropMarkerLeft = frozenColumnsWidth; + } else { + dropMarkerLeft = -10000000; + } + } else if (dropMarkerLeft > header.getElement().getOffsetWidth() || dropMarkerLeft < 0) { dropMarkerLeft = -10000000; } @@ -2971,6 +2980,11 @@ public class Grid extends ResizeComposite implements private void resolveDragElementHorizontalPosition(final int clientX) { int left = clientX - table.getAbsoluteLeft(); left = Math.max(0, Math.min(left, table.getClientWidth())); + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (left < frozenColumnsWidth) { + left = (int) frozenColumnsWidth; + } + left -= dragElement.getClientWidth() / 2; dragElement.getStyle().setLeft(left, Unit.PX); } @@ -3071,6 +3085,13 @@ public class Grid extends ResizeComposite implements autoScroller.stop(); } + private double getFrozenColumnsWidth() { + double result = 0.0d; + for (int i = 0; i < getFrozenColumnCount(); i++) { + result += getColumn(i).getWidthActual(); + } + return result; + } }; /** diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java index e7fbc14d89..d387bbe22f 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java @@ -140,6 +140,11 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { getGridElement().getCell(row, column).click(); } + protected void setFrozenColumns(int numberOfFrozenColumns) { + selectMenuPath("Component", "State", "Frozen column count", + Integer.toString(numberOfFrozenColumns)); + } + protected void assertColumnHeaderOrder(int... indices) { List headers = getGridHeaderRowCells(); for (int i = 0; i < indices.length; i++) { @@ -158,7 +163,7 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { return headerRowCells.get(index); } - protected void dragDefaultColumnHeader(int draggedColumnHeaderIndex, + protected void dragAndDropDefaultColumnHeader(int draggedColumnHeaderIndex, int onTopOfColumnHeaderIndex, int xOffsetFromColumnTopLeftCorner) { new Actions(getDriver()) .clickAndHold(getDefaultColumnHeader(draggedColumnHeaderIndex)) diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java index 9f43c39b1e..ac267dc42e 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -131,7 +131,7 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { sortColumn(0); // when - dragDefaultColumnHeader(0, 2, 10); + dragAndDropDefaultColumnHeader(0, 2, 10); // then assertColumnIsSorted(1); @@ -158,7 +158,7 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { focusDefaultHeader(0); // when - dragDefaultColumnHeader(0, 3, 10); + dragAndDropDefaultColumnHeader(0, 3, 10); // then WebElement defaultColumnHeader = getDefaultColumnHeader(2); @@ -173,7 +173,7 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { focusCell(2, 2); // when - dragDefaultColumnHeader(2, 0, 10); + dragAndDropDefaultColumnHeader(2, 0, 10); // then assertFocusedCell(2, 0); @@ -186,7 +186,7 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { focusCell(1, 3); // when - dragDefaultColumnHeader(4, 1, 10); + dragAndDropDefaultColumnHeader(4, 1, 10); // then assertFocusedCell(1, 4); @@ -199,7 +199,7 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { focusCell(4, 2); // when - dragDefaultColumnHeader(0, 4, 10); + dragAndDropDefaultColumnHeader(0, 4, 10); // then assertFocusedCell(4, 1); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java index 2cc8610209..7ab66aad95 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java @@ -48,7 +48,7 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { toggleColumnReordering(); // when - dragDefaultColumnHeader(0, 2, 10); + dragAndDropDefaultColumnHeader(0, 2, 10); // then assertColumnHeaderOrder(1, 0, 2); @@ -62,7 +62,7 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { toggleColumnReordering(); // when - dragDefaultColumnHeader(0, 2, 110); + dragAndDropDefaultColumnHeader(0, 2, 110); // then assertColumnHeaderOrder(1, 2, 0); @@ -76,13 +76,13 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { toggleColumnReordering(); // when - dragDefaultColumnHeader(2, 0, 10); + dragAndDropDefaultColumnHeader(2, 0, 10); // then assertColumnHeaderOrder(2, 0, 1, 3); // when - dragDefaultColumnHeader(1, 3, 110); + dragAndDropDefaultColumnHeader(1, 3, 110); // then assertColumnHeaderOrder(2, 1, 3, 0); @@ -95,7 +95,7 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertColumnHeaderOrder(0, 1, 2); // when - dragDefaultColumnHeader(0, 2, 110); + dragAndDropDefaultColumnHeader(0, 2, 110); // then assertColumnHeaderOrder(0, 1, 2); @@ -109,7 +109,7 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { toggleColumnReordering(); // when - dragDefaultColumnHeader(0, 2, 10); + dragAndDropDefaultColumnHeader(0, 2, 10); assertColumnHeaderOrder(1, 0, 2); moveColumnManuallyLeftByOne(0); @@ -127,7 +127,7 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { // when selectMenuPath(new String[] { "Component", "Internals", "Update column order without updating client" }); - dragDefaultColumnHeader(2, 0, 10); + dragAndDropDefaultColumnHeader(2, 0, 10); // then assertColumnHeaderOrder(1, 0, 2); @@ -141,7 +141,7 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { // when toggleColumnReorderListener(); - dragDefaultColumnHeader(0, 2, 10); + dragAndDropDefaultColumnHeader(0, 2, 10); // then assertColumnReorderEvent(true); @@ -152,19 +152,19 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { // given openTestURL(); toggleColumnReordering(); - dragDefaultColumnHeader(0, 2, 10); + dragAndDropDefaultColumnHeader(0, 2, 10); assertNoColumnReorderEvent(); // when toggleColumnReorderListener(); - dragDefaultColumnHeader(0, 2, 110); + dragAndDropDefaultColumnHeader(0, 2, 110); // then assertColumnReorderEvent(true); // when toggleColumnReorderListener(); - dragDefaultColumnHeader(0, 3, 10); + dragAndDropDefaultColumnHeader(0, 3, 10); // then assertNoColumnReorderEvent(); @@ -182,6 +182,36 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertColumnReorderEvent(false); } + @Test + public void testColumnReorder_draggingFrozenColumns_impossible() { + // given + openTestURL(); + toggleColumnReordering(); + setFrozenColumns(2); + assertColumnHeaderOrder(0, 1, 2, 3); + + // when + dragAndDropDefaultColumnHeader(0, 2, 10); + + // then + assertColumnHeaderOrder(0, 1, 2, 3); + } + + @Test + public void testColumnReorder_draggingColumnOnTopOfFrozenColumn_columnDroppedRightOfFrozenColumns() { + // given + openTestURL(); + toggleColumnReordering(); + setFrozenColumns(1); + assertColumnHeaderOrder(0, 1, 2, 3); + + // when + dragAndDropDefaultColumnHeader(2, 0, 10); + + // then + assertColumnHeaderOrder(0, 2, 1, 3); + } + private void toggleColumnReordering() { selectMenuPath(COLUMN_REORDERING_PATH); } -- cgit v1.2.3 From 0a78453dfa920768f2f6a45b27eb005826530677 Mon Sep 17 00:00:00 2001 From: Leif Åstrand Date: Tue, 24 Feb 2015 14:51:58 +0200 Subject: Don't reference private class in javadoc (#16644) Change-Id: I27e6ab90790fc2f4c39679a1e7f69c6ecf13a276 --- client/src/com/vaadin/client/widget/escalator/Spacer.java | 3 ++- client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/escalator/Spacer.java b/client/src/com/vaadin/client/widget/escalator/Spacer.java index 0b0a2b257f..371d539f57 100644 --- a/client/src/com/vaadin/client/widget/escalator/Spacer.java +++ b/client/src/com/vaadin/client/widget/escalator/Spacer.java @@ -16,9 +16,10 @@ package com.vaadin.client.widget.escalator; import com.google.gwt.dom.client.Element; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; /** - * A representation of a spacer element in a {@link SpacerContainer}. + * A representation of a spacer element in a {@link BodyRowContainer}. * * @since * @author Vaadin Ltd diff --git a/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java index 18f53db507..01d715fdd0 100644 --- a/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java +++ b/client/src/com/vaadin/client/widget/escalator/SpacerUpdater.java @@ -15,6 +15,8 @@ */ package com.vaadin.client.widget.escalator; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; + /** * An interface that handles the display of content for spacers. *

@@ -24,7 +26,7 @@ package com.vaadin.client.widget.escalator; * @since * @author Vaadin Ltd * @see Spacer - * @see SpacerContainer + * @see BodyRowContainer */ public interface SpacerUpdater { -- cgit v1.2.3 From fa50ea28870436c5dedb459e364d24f14f7ab001 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Thu, 26 Feb 2015 09:44:23 +0200 Subject: Support column reordering from all header rows. (#16643) Change-Id: I78a4a50b011576a026518a58c36822a383979986 --- WebContent/VAADIN/themes/base/grid/grid.scss | 3 ++- client/src/com/vaadin/client/widgets/Grid.java | 14 +++++----- .../grid/basicfeatures/GridBasicFeaturesTest.java | 19 +++++++++++-- .../server/GridColumnReorderTest.java | 31 ++++++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) (limited to 'client') diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss index 35024e27c0..20f8478885 100644 --- a/WebContent/VAADIN/themes/base/grid/grid.scss +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -55,11 +55,12 @@ $v-grid-editor-background-color: $v-grid-row-background-color !default; .#{$primaryStyleName} .header-drag-table { border-spacing: 0; + position: relative; table-layout: fixed; width: inherit; // a decent default fallback .#{$primaryStyleName}-header { - + position: absolute; > .#{$primaryStyleName}-cell { border: $v-grid-border; margin-top: -10px; diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index fc208843e4..ebc2fcf97e 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -2930,6 +2930,13 @@ public class Grid extends ResizeComposite implements 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); } @@ -5375,17 +5382,12 @@ public class Grid extends ResizeComposite implements private boolean handleHeaderCellDragStartEvent(Event event, RowContainer container) { - if (!columnReorderingAllowed) { + if (!isColumnReorderingAllowed()) { return false; } if (container != escalator.getHeader()) { return false; } - // for now only support reordering of default row as the only row - if (!getHeader().getRow(eventCell.getRowIndex()).isDefault() - || getHeader().getRowCount() != 1) { - return false; - } if (eventCell.getColumnIndex() < escalator.getColumnConfiguration() .getFrozenColumnCount()) { return false; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java index d387bbe22f..13f32fd2a2 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java @@ -30,6 +30,7 @@ import org.openqa.selenium.remote.DesiredCapabilities; import com.vaadin.testbench.TestBenchElement; import com.vaadin.testbench.elements.GridElement; +import com.vaadin.testbench.elements.GridElement.GridCellElement; import com.vaadin.tests.annotations.TestCategory; import com.vaadin.tests.tb3.MultiBrowserTest; @@ -158,8 +159,9 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { .getText().toLowerCase()); } - protected WebElement getDefaultColumnHeader(int index) { - List headerRowCells = getGridHeaderRowCells(); + protected GridCellElement getDefaultColumnHeader(int index) { + List headerRowCells = getGridElement().getHeaderCells( + 0); return headerRowCells.get(index); } @@ -172,6 +174,19 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { xOffsetFromColumnTopLeftCorner, 0).release().perform(); } + protected void dragAndDropColumnHeader(int headerRow, + int draggedColumnHeaderIndex, int onTopOfColumnHeaderIndex, + int xOffsetFromColumnTopLeftCorner) { + new Actions(getDriver()) + .clickAndHold( + getGridElement().getHeaderCell(headerRow, + draggedColumnHeaderIndex)) + .moveToElement( + getGridElement().getHeaderCell(headerRow, + onTopOfColumnHeaderIndex), + xOffsetFromColumnTopLeftCorner, 0).release().perform(); + } + protected void assertColumnIsSorted(int index) { WebElement columnHeader = getDefaultColumnHeader(index); assertTrue(columnHeader.getAttribute("class").contains("sort")); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java index 7ab66aad95..ae0ab62aab 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java @@ -212,6 +212,37 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertColumnHeaderOrder(0, 2, 1, 3); } + @Test + public void testColumnReordering_twoHeaderRows_dndReorderingPossibleFromFirstRow() { + // given + openTestURL(); + toggleColumnReordering(); + selectMenuPath("Component", "Header", "Append row"); + assertColumnHeaderOrder(0, 1, 2, 3); + + // when + dragAndDropColumnHeader(0, 0, 2, 100); + + // then + assertColumnHeaderOrder(1, 2, 0, 3); + } + + @Test + public void testColumnReordering_twoHeaderRows_dndReorderingPossibleFromSecondRow() { + // given + openTestURL(); + toggleColumnReordering(); + selectMenuPath("Component", "Header", "Append row"); + assertColumnHeaderOrder(0, 1, 2, 3); + + // when + // when + dragAndDropColumnHeader(1, 0, 2, 100); + + // then + assertColumnHeaderOrder(1, 2, 0, 3); + } + private void toggleColumnReordering() { selectMenuPath(COLUMN_REORDERING_PATH); } -- cgit v1.2.3 From c521daf78b0897b31ca516c10ceb092ffdca43f2 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Wed, 25 Feb 2015 10:24:01 +0200 Subject: Adds support for inserting rows to Escalator while spacers are open (#16644) Change-Id: Id8689ed308926debee385e158d045f014fae21e0 --- .../src/com/vaadin/client/widgets/Escalator.java | 215 ++++++++++++++++----- 1 file changed, 172 insertions(+), 43 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index dcc6c98960..30ecf64ff4 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -17,6 +17,7 @@ package com.vaadin.client.widgets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -2569,15 +2570,14 @@ public class Escalator extends Widget implements RequiresResize, double viewportPx = y2 - y1; double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1, - SpacerMeasurementStrategy.PARTIAL, y2, - SpacerMeasurementStrategy.PARTIAL); + SpacerInclusionStrategy.PARTIAL, y2, + SpacerInclusionStrategy.PARTIAL); return viewportPx - spacerPx; } private int getLogicalRowIndex(final double px) { - double rowPx = px - - spacerContainer.getSpacerHeightsSumUntilPx(px); + double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px); return (int) (rowPx / getDefaultRowHeight()); } @@ -2587,9 +2587,6 @@ public class Escalator extends Widget implements RequiresResize, return; } - // TODO - getLogger().warning("[[spacers]] inserting rows"); - /* * TODO: this method should probably only add physical rows, and not * populate them - let everything be populated as appropriate by the @@ -2635,6 +2632,13 @@ public class Escalator extends Widget implements RequiresResize, final int visualOffset = getLogicalRowIndex(visualRowOrder .getFirst()); + final double pxDiff = numberOfRows * getDefaultRowHeight(); + for (SpacerContainer.SpacerImpl spacer : spacerContainer + .getSpacersForRowAndAfter(index)) { + spacer.setPositionDiff(0, pxDiff); + spacer.setRowIndex(spacer.getRow() + numberOfRows); + } + /* * At this point, we have added new escalator rows, if so * needed. @@ -2646,7 +2650,7 @@ public class Escalator extends Widget implements RequiresResize, final int rowsStillNeeded = numberOfRows - addedRows.size(); final Range unupdatedVisual = convertToVisual(Range.withLength( unupdatedLogicalStart, rowsStillNeeded)); - final int end = root.getChildCount(); + final int end = getEscalatorRowCount(); final int start = end - unupdatedVisual.length(); final int visualTargetIndex = unupdatedLogicalStart - visualOffset; @@ -2658,7 +2662,16 @@ public class Escalator extends Widget implements RequiresResize, * getDefaultRowHeight(); final ListIterator i = visualRowOrder .listIterator(visualTargetIndex + (end - start)); + + int logicalRowIndexCursor = unupdatedLogicalStart; while (i.hasNext()) { + SpacerContainer.SpacerImpl spacer = spacerContainer + .getSpacer(logicalRowIndexCursor); + if (spacer != null) { + rowTop += spacer.getHeight(); + } + logicalRowIndexCursor++; + final TableRowElement tr = i.next(); setRowPosition(tr, 0, rowTop); rowTop += getDefaultRowHeight(); @@ -2698,10 +2711,11 @@ public class Escalator extends Widget implements RequiresResize, assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was " + visualTargetIndex + ")"; - assert visualTargetIndex <= root.getChildCount() : "Visual target " + assert visualTargetIndex <= getEscalatorRowCount() : "Visual target " + "must not be greater than the number of escalator rows (was " - + visualTargetIndex + ", escalator rows " - + root.getChildCount() + ")"; + + visualTargetIndex + + ", escalator rows " + + getEscalatorRowCount() + ")"; assert logicalTargetIndex + visualSourceRange.length() <= getRowCount() : "Logical " + "target leads to rows outside of the data range (" @@ -2784,12 +2798,11 @@ public class Escalator extends Widget implements RequiresResize, /** * Adjust the scroll position and move the contained rows. *

- * Note: This method does not account for spacers. - *

* The difference between using this method and simply scrolling is that - * this method "takes the rows with it" and renders them appropriately. - * The viewport may be scrolled any arbitrary amount, and the rows are - * moved appropriately, but always snapped into a plausible place. + * 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. *

*

*
Example 1
@@ -2809,25 +2822,31 @@ public class Escalator extends Widget implements RequiresResize, */ public void moveViewportAndContent(final double yDelta) { - /* - * TODO: When adding and removing rows starts supporting spacers, - * this method should also take spacers into account. Remember to - * adjust the javadoc as well. - */ - if (yDelta == 0) { return; } double newTop = tBodyScrollTop + yDelta; - final double rowTopPos = body.getRowTop(getLogicalRowIndex(newTop)); - verticalScrollbar.setScrollPos(newTop); - for (int i = 0; i < visualRowOrder.size(); i++) { - final TableRowElement tr = visualRowOrder.get(i); - setRowPosition(tr, 0, rowTopPos + getDefaultRowHeight() * i); + final double defaultRowHeight = getDefaultRowHeight(); + double rowPxDelta = yDelta - (yDelta % defaultRowHeight); + int rowIndexDelta = (int) (yDelta / defaultRowHeight); + if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) { + + Collection 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, newTop); } @@ -2850,7 +2869,7 @@ public class Escalator extends Widget implements RequiresResize, final int index, final int numberOfRows) { final int escalatorRowsStillFit = getMaxEscalatorRowCapacity() - - root.getChildCount(); + - getEscalatorRowCount(); final int escalatorRowsNeeded = Math.min(numberOfRows, escalatorRowsStillFit); @@ -3802,6 +3821,21 @@ public class Escalator extends Widget implements RequiresResize, return Collections.unmodifiableList(sublist); } } + + /** + * This method calculates the current escalator row count directly from + * the DOM. + *

+ * 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 + */ + private int getEscalatorRowCount() { + return root.getChildCount() + - spacerContainer.getSpacersInDom().size(); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4280,15 +4314,18 @@ public class Escalator extends Widget implements RequiresResize, /** * A decision on how to measure a spacer when it is partially within a * designated range. + *

+ * The meaning of each value may differ depending on the context it is being + * used in. Check that particular method's JavaDoc. */ - public enum SpacerMeasurementStrategy { - /** Take the entire spacer's height into account. */ + public enum SpacerInclusionStrategy { + /** A representation of "the entire spacer". */ COMPLETE, - /** Take the visible part into account. */ + /** A representation of "a partial spacer". */ PARTIAL, - /** Exclude the entire spacer. */ + /** A representation of "no spacer at all". */ NONE } @@ -4300,7 +4337,7 @@ public class Escalator extends Widget implements RequiresResize, private final class SpacerImpl implements Spacer { private TableCellElement spacerElement; private TableRowElement root; - private final int rowIndex; + private int rowIndex; private double height = -1; private boolean domHasBeenSetup = false; @@ -4313,6 +4350,15 @@ public class Escalator extends Widget implements RequiresResize, root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); } + public void setPositionDiff(double x, double y) { + setPosition(getLeft() + x, getTop() + y); + } + + public double getLeft() { + // not implemented yet. + return 0; + } + 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."; @@ -4352,7 +4398,7 @@ public class Escalator extends Widget implements RequiresResize, root.getStyle().setHeight(height, Unit.PX); // move the visible spacers getRow row onwards. - shiftSpacerPositions(getRow(), heightDiff); + shiftSpacerPositionsAfterRow(getRow(), heightDiff); /* * If we're growing, we'll adjust the scroll size first, then @@ -4439,6 +4485,14 @@ public class Escalator extends Widget implements RequiresResize, public double getTop() { return positions.getTop(getRootElement()); } + + @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; + rowIndexToSpacer.put(this.rowIndex, this); + } } private final TreeMap rowIndexToSpacer = new TreeMap(); @@ -4465,26 +4519,100 @@ public class Escalator extends Widget implements RequiresResize, } } + @SuppressWarnings("boxing") + public Collection getSpacersForRowAndAfter( + int logicalRowIndex) { + return new ArrayList(rowIndexToSpacer.tailMap( + logicalRowIndex, true).values()); + } + + /** + * Get all spacers from one pixel point onwards. + *

+ * + * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + *

+ *
{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + *
include the spacer + *
{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + *
include the spacer + *
{@link SpacerInclusionStrategy#NONE NONE} + *
ignore the spacer + *
+ * + * @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 getSpacersAfterPx(final double px, + final SpacerInclusionStrategy strategy) { + + ArrayList spacers = new ArrayList( + 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 getSpacersInDom() { + return Collections + .unmodifiableCollection(rowIndexToSpacer.values()); + } + /** * Gets the amount of pixels occupied by spacers between two pixel * points. + *

+ * In this method, the {@link SpacerInclusionStrategy} has the following + * meaning when a spacer lies in the middle of either pixel argument: + *

+ *
{@link SpacerInclusionStrategy#COMPLETE COMPLETE} + *
take the entire spacer into account + *
{@link SpacerInclusionStrategy#PARTIAL PARTIAL} + *
take only the visible area into account + *
{@link SpacerInclusionStrategy#NONE NONE} + *
ignore that spacer + *
* * @param rangeTop * the top pixel point * @param topInclusion - * how to measure a spacer that happens to lie in the middle - * of {@code rangeTop}. + * the inclusion strategy regarding {@code rangeTop}. * @param rangeBottom * the bottom pixel point * @param bottomInclusion - * how to measure a spacer that happens to lie in the middle - * of {@code rangeBottom}. + * the inclusion strategy regarding {@code rangeBottom}. * @return the pixels occupied by spacers between {@code rangeTop} and * {@code rangeBottom} */ public double getSpacerHeightsSumBetweenPx(double rangeTop, - SpacerMeasurementStrategy topInclusion, double rangeBottom, - SpacerMeasurementStrategy bottomInclusion) { + SpacerInclusionStrategy topInclusion, double rangeBottom, + SpacerInclusionStrategy bottomInclusion) { assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom"; @@ -4596,8 +4724,8 @@ public class Escalator extends Widget implements RequiresResize, */ public double getSpacerHeightsSumUntilPx(double px) { return getSpacerHeightsSumBetweenPx(0, - SpacerMeasurementStrategy.PARTIAL, px, - SpacerMeasurementStrategy.PARTIAL); + SpacerInclusionStrategy.PARTIAL, px, + SpacerInclusionStrategy.PARTIAL); } /** @@ -4733,7 +4861,8 @@ public class Escalator extends Widget implements RequiresResize, } @SuppressWarnings("boxing") - private void shiftSpacerPositions(int changedRowIndex, double diffPx) { + private void shiftSpacerPositionsAfterRow(int changedRowIndex, + double diffPx) { for (SpacerImpl spacer : rowIndexToSpacer.tailMap(changedRowIndex, false).values()) { spacer.setPosition(0, spacer.getTop() + diffPx); -- cgit v1.2.3 From 4ba54083a8a4e0aeada8a558c3c817c66b67a95c Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Thu, 26 Feb 2015 09:32:43 +0200 Subject: Fixes two bugs when inserting escalator rows with spacers open (#16644) If the viewport is not yet filled with escalator rows, it didn't account for spacers. Also, the scrollbar was updated incorrectly after adding new rows. Change-Id: Id9cab71c2c4b82331771d1243143eb9db0883a6c --- .../src/com/vaadin/client/widgets/Escalator.java | 157 +++++++++++++-------- .../EscalatorBasicClientFeaturesTest.java | 2 + .../escalator/EscalatorSpacerTest.java | 51 ++++++- 3 files changed, 152 insertions(+), 58 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 30ecf64ff4..0df078d26e 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -820,7 +820,8 @@ public class Escalator extends Widget implements RequiresResize, * 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; @@ -2420,6 +2421,13 @@ public class Escalator extends Widget implements RequiresResize, return; } + /* + * TODO This will break the logical index calculation, as it will + * try to search for non- + */ + getLogger().warning( + "[[spacers]] scrolling and spacers near the bottom"); + boolean rowsWereMoved = false; final double topElementPosition; @@ -2587,6 +2595,13 @@ public class Escalator extends Widget implements RequiresResize, return; } + final double pxDiff = numberOfRows * getDefaultRowHeight(); + for (SpacerContainer.SpacerImpl spacer : spacerContainer + .getSpacersForRowAndAfter(index)) { + spacer.setPositionDiff(0, pxDiff); + spacer.setRowIndex(spacer.getRow() + numberOfRows); + } + /* * TODO: this method should probably only add physical rows, and not * populate them - let everything be populated as appropriate by the @@ -2632,13 +2647,6 @@ public class Escalator extends Widget implements RequiresResize, final int visualOffset = getLogicalRowIndex(visualRowOrder .getFirst()); - final double pxDiff = numberOfRows * getDefaultRowHeight(); - for (SpacerContainer.SpacerImpl spacer : spacerContainer - .getSpacersForRowAndAfter(index)) { - spacer.setPositionDiff(0, pxDiff); - spacer.setRowIndex(spacer.getRow() + numberOfRows); - } - /* * At this point, we have added new escalator rows, if so * needed. @@ -2648,33 +2656,33 @@ public class Escalator extends Widget implements RequiresResize, * the remaining rows aswell. */ final int rowsStillNeeded = numberOfRows - addedRows.size(); - 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 i = visualRowOrder - .listIterator(visualTargetIndex + (end - start)); - - int logicalRowIndexCursor = unupdatedLogicalStart; - while (i.hasNext()) { - SpacerContainer.SpacerImpl spacer = spacerContainer - .getSpacer(logicalRowIndexCursor); - if (spacer != null) { - rowTop += spacer.getHeight(); - } - logicalRowIndexCursor++; - final TableRowElement tr = i.next(); - setRowPosition(tr, 0, rowTop); - 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 i = visualRowOrder + .listIterator(visualTargetIndex + (end - start)); + + int logicalRowIndexCursor = unupdatedLogicalStart; + while (i.hasNext()) { + rowTop += spacerContainer + .getSpacerHeight(logicalRowIndexCursor++); + + final TableRowElement tr = i.next(); + getLogger().warning("iterate"); + setRowPosition(tr, 0, rowTop); + rowTop += getDefaultRowHeight(); + } } fireRowVisibilityChangeEvent(); @@ -2786,11 +2794,8 @@ public class Escalator extends Widget implements RequiresResize, setRowPosition(tr, 0, newRowTop); newRowTop += getDefaultRowHeight(); - SpacerContainer.SpacerImpl spacer = spacerContainer - .getSpacer(logicalTargetIndex + i); - if (spacer != null) { - newRowTop += spacer.getHeight(); - } + newRowTop += spacerContainer + .getSpacerHeight(logicalTargetIndex + i); } } } @@ -2879,25 +2884,29 @@ public class Escalator extends Widget implements RequiresResize, 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++) { - setRowPosition(addedRows.get(i), 0, (index + i) - * getDefaultRowHeight()); - } + double y = index * getDefaultRowHeight() + + spacerContainer.getSpacerHeightsSumUntilIndex(index); + for (int i = index; i < visualRowOrder.size(); i++) { + + final TableRowElement tr; + if (i - index < addedRows.size()) { + tr = addedRows.get(i - index); + } else { + tr = visualRowOrder.get(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); - setRowPosition(tr, 0, i * getDefaultRowHeight()); + getLogger().warning("y: " + y + ", index: " + i); + setRowPosition(tr, 0, y); + y += getDefaultRowHeight(); + double spacerHeight = spacerContainer.getSpacerHeight(i); + getLogger().warning( + "height: " + spacerHeight + ", index: " + i); + y += spacerHeight; } return addedRows; } else { - return new ArrayList(); + return Collections.emptyList(); } } @@ -4519,6 +4528,23 @@ public class Escalator extends Widget implements RequiresResize, } } + /** + * 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 getSpacersForRowAndAfter( int logicalRowIndex) { @@ -4738,14 +4764,35 @@ public class Escalator extends Widget implements RequiresResize, */ @SuppressWarnings("boxing") public double getSpacerHeightsSumUntilIndex(int logicalIndex) { + return getHeights(rowIndexToSpacer.headMap(logicalIndex, false) + .values()); + } + + private double getHeights(Collection spacers) { double heights = 0; - for (SpacerImpl spacer : rowIndexToSpacer.headMap(logicalIndex, - false).values()) { + 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)); } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index ef2d605928..c22da47d62 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -38,8 +38,10 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String COLUMNS = "Columns"; protected static final String ADD_ONE_COLUMN_TO_BEGINNING = "Add one column to beginning"; protected static final String ADD_ONE_ROW_TO_BEGINNING = "Add one row to beginning"; + protected static final String ADD_ONE_ROW_TO_END = "Add one row to end"; protected static final String REMOVE_ONE_COLUMN_FROM_BEGINNING = "Remove one column from beginning"; protected static final String REMOVE_ONE_ROW_FROM_BEGINNING = "Remove one row from beginning"; + protected static final String REMOVE_ALL_ROWS = "Remove all rows"; protected static final String REMOVE_50_ROWS_FROM_BOTTOM = "Remove 50 rows from bottom"; protected static final String REMOVE_50_ROWS_FROM_ALMOST_BOTTOM = "Remove 50 rows from almost bottom"; protected static final String ADD_ONE_OF_EACH_ROW = "Add one of each row"; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index 43a18b507d..1985de1c82 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -25,10 +25,12 @@ import java.util.regex.Pattern; import org.junit.Before; import org.junit.Test; +import org.openqa.selenium.WebElement; -import com.vaadin.testbench.TestBenchElement; +import com.vaadin.client.WidgetUtil; import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; +@SuppressWarnings("boxing") public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { //@formatter:off @@ -98,7 +100,6 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { } @Test - @SuppressWarnings("boxing") public void spacerPushesVisibleRowsDown() { double oldTop = getElementTop(getBodyRow(2)); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); @@ -107,7 +108,51 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { assertGreater("Row below a spacer was not pushed down", newTop, oldTop); } - private static double getElementTop(TestBenchElement element) { + @Test + public void addingRowAboveSpacerPushesItDown() { + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + double oldTop = getElementTop(getSpacer(1)); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + double newTop = getElementTop(getSpacer(1)); + + assertGreater("Spacer should've been pushed down", newTop, oldTop); + } + + @Test + public void addingRowBelowSpacerDoesNotPushItDown() { + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + double oldTop = getElementTop(getSpacer(1)); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END); + double newTop = getElementTop(getSpacer(1)); + + assertEquals("Spacer should've not been pushed down", newTop, oldTop, + WidgetUtil.PIXEL_EPSILON); + } + + @Test + public void addingRowBelowSpacerIsActuallyRenderedBelowWhenEscalatorIsEmpty() { + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); + + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + double spacerTop = getElementTop(getSpacer(1)); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END); + double rowTop = getElementTop(getBodyRow(2)); + + assertEquals("Next row should've been rendered below the spacer", + spacerTop + 100, rowTop, WidgetUtil.PIXEL_EPSILON); + } + + private static double getElementTop(WebElement element) { /* * we need to parse the style attribute, since using getCssValue gets a * normalized value that is harder to parse. -- cgit v1.2.3 From 31b188e370b60832ecc05df8ddd41a6884996aad Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Thu, 26 Feb 2015 16:23:32 +0200 Subject: Handle spanned header cells when dnd reordering columns in grid. (#16643) Prevents dragging columns from outside spanned cells to inside them, on any row. Currently prevents dragging columns inside spanned cells. Will change this later. Change-Id: Ie832b3c404a3afbcce0374f8b5088dc8cb124fb8 --- .../vaadin/client/widget/grid/AutoScroller.java | 1 + client/src/com/vaadin/client/widgets/Grid.java | 129 +++++++++++++++++---- .../basicfeatures/GridBasicClientFeaturesTest.java | 13 ++- .../grid/basicfeatures/GridColumnReorderTest.java | 30 +++++ 4 files changed, 147 insertions(+), 26 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/grid/AutoScroller.java b/client/src/com/vaadin/client/widget/grid/AutoScroller.java index 60c9535ca7..71851d67c8 100644 --- a/client/src/com/vaadin/client/widget/grid/AutoScroller.java +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -669,6 +669,7 @@ public class AutoScroller { } private double getFrozenColumnsWidth() { + // TODO handle the case where the checkbox column is present double value = 0; for (int i = 0; i < grid.getFrozenColumnCount(); i++) { value += grid.getColumn(i).getWidthActual(); diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index ebc2fcf97e..3fd119d9ea 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; @@ -37,7 +39,6 @@ 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.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; @@ -415,6 +416,16 @@ public class Grid extends ResizeComposite implements return cells.get(column); } + /** + * Returns true if this row contains spanned cells. + * + * @since + * @return does this row contain spanned cells + */ + public boolean hasSpannedCells() { + return !cellGroups.isEmpty(); + } + /** * Merges columns cells in a row * @@ -2906,6 +2917,11 @@ public class Grid extends ResizeComposite implements 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 possibleDropPositions = new TreeMap(); /** * Makes sure that drag cancel doesn't cause anything unwanted like sort */ @@ -2952,33 +2968,33 @@ public class Grid extends ResizeComposite implements } private void updateDragDropMarker(final int clientX) { - RowContainer header = escalator.getHeader(); - NodeList cells = header.getRowElement( - eventCell.getRowIndex()).getCells(); - double dropMarkerLeft = 0 - escalator.getScrollLeft(); - latestColumnDropIndex = 0; - for (int i = 0; i < cells.getLength(); i++, latestColumnDropIndex++) { - TableCellElement cellElement = cells.getItem(i); - int cellX = cellElement.getAbsoluteLeft(); - int cellWidth = cellElement.getOffsetWidth(); - if (clientX < cellX || clientX < cellX + (cellWidth / 2)) { - break; - } else { - dropMarkerLeft += cellWidth; - } + final double scrollLeft = getScrollLeft(); + final double cursorXCoordinate = clientX + - escalator.getHeader().getElement().getAbsoluteLeft(); + final Entry cellEdgeOnRight = possibleDropPositions + .ceilingEntry(cursorXCoordinate); + final Entry 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) { - latestColumnDropIndex = getFrozenColumnCount(); - if (getScrollLeft() == 0) { - dropMarkerLeft = frozenColumnsWidth; - } else { - dropMarkerLeft = -10000000; - } - } else if (dropMarkerLeft > header.getElement().getOffsetWidth() - || dropMarkerLeft < 0) { + if (dropMarkerLeft < frozenColumnsWidth + || dropMarkerLeft > escalator.getHeader().getElement() + .getOffsetWidth() || dropMarkerLeft < 0) { dropMarkerLeft = -10000000; } dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); @@ -3009,6 +3025,9 @@ public class Grid extends ResizeComposite implements eventCell.getElement().addClassName("dragged"); // mark the floating cell, for styling & testing dragElement.addClassName("dragged-column-header"); + + calculatePossibleDropPositions(); + // start the auto scroll handler autoScroller.setScrollAreaPX(60); autoScroller.start(startingEvent, ScrollAxis.HORIZONTAL, @@ -3099,6 +3118,58 @@ public class Grid extends ResizeComposite implements } return result; } + + private void calculatePossibleDropPositions() { + possibleDropPositions.clear(); + + if (!calculatePossibleDropPositionInsideSpannedHeader()) { + HashMap columnIndexToDropPositionMap = new HashMap(); + + final int frozenColumns = Math.max(0, getFrozenColumnCount()); + double position = getFrozenColumnsWidth(); + // add all columns except frozen columns + // TODO handle the case where the checkbox column is present + for (int i = frozenColumns; i < getColumnCount(); i++) { + columnIndexToDropPositionMap.put(i, position); + position += getColumn(i).getWidthActual(); + } + // add the right side of the last column as columns.size() + columnIndexToDropPositionMap.put(getColumnCount(), position); + + // can't drop inside a spanned header from outside it + // -> remove all column indexes that are inside a spanned cell + // in any header row + for (int c = frozenColumns; c < getColumnCount(); c++) { + for (int r = 0; r < getHeaderRowCount(); r++) { + HeaderRow headerRow = getHeaderRow(r); + if (headerRow.hasSpannedCells()) { + HeaderCell cell = headerRow.getCell(getColumn(c)); + assert cell != null : "Somehow got a null cell for row:cell " + + r + ":" + c; + int colspan = cell.getColspan(); + while (colspan > 1) { + c++; + colspan--; + columnIndexToDropPositionMap.remove(Integer + .valueOf(c)); + } + } + } + } + // finally lets flip the map, because while dragging we want the + // column index matching the X-coordinate + for (Entry entry : columnIndexToDropPositionMap + .entrySet()) { + possibleDropPositions.put(entry.getValue(), entry.getKey()); + } + } + } + + private boolean calculatePossibleDropPositionInsideSpannedHeader() { + // TODO if the dragged column is inside a spanned header on any row, + // then dragging is limited to inside that spanned cell + return false; + } }; /** @@ -5392,6 +5463,16 @@ public class Grid extends ResizeComposite implements .getFrozenColumnCount()) { return false; } + // for now prevent dragging of spanned cells + if (eventCell.getElement().getColSpan() > 1) { + return false; + } + // for now prevent dragging of columns belonging to a spanned cell + for (int r = 0; r < getHeaderRowCount(); r++) { + if (getHeaderRow(r).getCell(eventCell.getColumn()).getColspan() > 1) { + return false; + } + } if (event.getTypeInt() == Event.ONMOUSEDOWN && event.getButton() == NativeEvent.BUTTON_LEFT || event.getTypeInt() == Event.ONTOUCHSTART) { diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java index d0e076fd3b..8e07729719 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java @@ -15,6 +15,8 @@ */ package com.vaadin.tests.components.grid.basicfeatures; +import java.util.List; + import org.openqa.selenium.Dimension; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; @@ -83,10 +85,17 @@ public abstract class GridBasicClientFeaturesTest extends GridBasicFeaturesTest if (composite) { // Composite requires the basic client features widget for subparts return ((TestBenchElement) findElement(By - .vaadin("//TestWidgetComponent"))) - .wrap(GridElement.class); + .vaadin("//TestWidgetComponent"))).wrap(GridElement.class); } else { return super.getGridElement(); } } + + @Override + protected void assertColumnHeaderOrder(int... indices) { + List headers = getGridHeaderRowCells(); + for (int i = 0; i < indices.length; i++) { + assertColumnHeader("HEADER (0," + indices[i] + ")", headers.get(i)); + } + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java index ac267dc42e..6e5eda43f1 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -205,6 +205,36 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { assertFocusedCell(4, 1); } + @Test + public void testColumnReorder_draggingHeaderRowThatHasColumnHeadersSpanned_cantDropInsideSpannedHeaderFromOutside() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(1, 3, 1, 80); + + // then + assertColumnHeaderOrder(0, 3, 1, 2, 4); + } + + @Test + public void testColumnReorder_anotherRowHasColumnHeadersSpanned_cantDropInsideSpannedHeaderFromOutside() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(0, 0, 2, 20); + + // then + assertColumnHeaderOrder(1, 2, 0, 3, 4); + } + private void toggleColumnReorder() { selectMenuPath("Component", "State", "Column Reordering"); } -- cgit v1.2.3 From 40916228681314c843b32e59fc91bbec18548332 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Fri, 27 Feb 2015 16:13:41 +0200 Subject: Handle selection column oddity when dnd reordering columns (#16443) Change-Id: I7bb20fb98a79e77acc9073bc9c2210d1dc3fd49e --- .../vaadin/client/connectors/GridConnector.java | 4 +- .../vaadin/client/widget/grid/AutoScroller.java | 13 +++- client/src/com/vaadin/client/widgets/Grid.java | 37 +++++++++-- .../server/GridColumnReorderTest.java | 75 +++++++++++++++++++++- 4 files changed, 120 insertions(+), 9 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index 2ae04b2be2..71450f6146 100644 --- a/client/src/com/vaadin/client/connectors/GridConnector.java +++ b/client/src/com/vaadin/client/connectors/GridConnector.java @@ -370,7 +370,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements List> columns = getWidget().getColumns(); final List newColumnOrder = new ArrayList(); for (Column column : columns) { - newColumnOrder.add(((CustomGridColumn) column).id); + if (column instanceof CustomGridColumn) { + newColumnOrder.add(((CustomGridColumn) column).id); + } // the other case would be the multi selection column } getRpcProxy(GridServerRpc.class).columnsReordered( newColumnOrder, columnOrder); diff --git a/client/src/com/vaadin/client/widget/grid/AutoScroller.java b/client/src/com/vaadin/client/widget/grid/AutoScroller.java index 71851d67c8..f7c80df623 100644 --- a/client/src/com/vaadin/client/widget/grid/AutoScroller.java +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -669,14 +669,23 @@ public class AutoScroller { } private double getFrozenColumnsWidth() { - // TODO handle the case where the checkbox column is present - double value = 0; + 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()); diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 3fd119d9ea..c237a7ba2f 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -3054,6 +3054,9 @@ public class Grid extends ResizeComposite implements latestColumnDropIndex--; } reordered.add(latestColumnDropIndex, moved); + reordered.remove(selectionColumn); // since setColumnOrder will + // add it anyway! + @SuppressWarnings("unchecked") Column[] array = reordered.toArray(new Column[reordered .size()]); @@ -3112,11 +3115,36 @@ public class Grid extends ResizeComposite implements } private double getFrozenColumnsWidth() { - double result = 0.0d; + double value = getMultiSelectColumnWidth(); for (int i = 0; i < getFrozenColumnCount(); i++) { - result += getColumn(i).getWidthActual(); + 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()); } - return result; } private void calculatePossibleDropPositions() { @@ -3125,10 +3153,9 @@ public class Grid extends ResizeComposite implements if (!calculatePossibleDropPositionInsideSpannedHeader()) { HashMap columnIndexToDropPositionMap = new HashMap(); - final int frozenColumns = Math.max(0, getFrozenColumnCount()); + final int frozenColumns = getSelectionAndFrozenColumnCount(); double position = getFrozenColumnsWidth(); // add all columns except frozen columns - // TODO handle the case where the checkbox column is present for (int i = frozenColumns; i < getColumnCount(); i++) { columnIndexToDropPositionMap.put(i, position); position += getColumn(i).getWidthActual(); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java index ae0ab62aab..c7113dbd5e 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridColumnReorderTest.java @@ -18,9 +18,12 @@ package com.vaadin.tests.components.grid.basicfeatures.server; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.List; + import org.junit.Before; import org.junit.Test; +import com.vaadin.testbench.TestBenchElement; import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; /** @@ -212,6 +215,77 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { assertColumnHeaderOrder(0, 2, 1, 3); } + @Test + public void testColumnReorder_draggingColumnLeftOfMultiSelectionColumn_columnDroppedRight() { + // given + openTestURL(); + toggleColumnReordering(); + selectMenuPath("Component", "State", "Selection mode", "multi"); + List gridHeaderRowCells = getGridHeaderRowCells(); + assertTrue(gridHeaderRowCells.get(0).getText().equals("")); + assertColumnHeader("Column 0", gridHeaderRowCells.get(1)); + assertColumnHeader("Column 1", gridHeaderRowCells.get(2)); + assertColumnHeader("Column 2", gridHeaderRowCells.get(3)); + + // when + dragAndDropDefaultColumnHeader(2, 0, 2); + + // then + gridHeaderRowCells = getGridHeaderRowCells(); + assertTrue(gridHeaderRowCells.get(0).getText().equals("")); + assertColumnHeader("Column 1", gridHeaderRowCells.get(1)); + assertColumnHeader("Column 0", gridHeaderRowCells.get(2)); + assertColumnHeader("Column 2", gridHeaderRowCells.get(3)); + } + + @Test + public void testColumnReorder_multiSelectionAndFrozenColumns_columnDroppedRight() { + // given + openTestURL(); + toggleColumnReordering(); + selectMenuPath("Component", "State", "Selection mode", "multi"); + setFrozenColumns(1); + List gridHeaderRowCells = getGridHeaderRowCells(); + assertTrue(gridHeaderRowCells.get(0).getText().equals("")); + assertColumnHeader("Column 0", gridHeaderRowCells.get(1)); + assertColumnHeader("Column 1", gridHeaderRowCells.get(2)); + assertColumnHeader("Column 2", gridHeaderRowCells.get(3)); + + // when + dragAndDropDefaultColumnHeader(3, 0, 2); + + // then + gridHeaderRowCells = getGridHeaderRowCells(); + assertTrue(gridHeaderRowCells.get(0).getText().equals("")); + assertColumnHeader("Column 0", gridHeaderRowCells.get(1)); + assertColumnHeader("Column 2", gridHeaderRowCells.get(2)); + assertColumnHeader("Column 1", gridHeaderRowCells.get(3)); + } + + @Test + public void testColumnReordering_multiSelectionColumnNotFrozen_stillCantDropLeftSide() { + // given + openTestURL(); + toggleColumnReordering(); + selectMenuPath("Component", "State", "Selection mode", "multi"); + setFrozenColumns(-1); + List gridHeaderRowCells = getGridHeaderRowCells(); + assertTrue(gridHeaderRowCells.get(0).getText().equals("")); + assertColumnHeader("Column 0", gridHeaderRowCells.get(1)); + assertColumnHeader("Column 1", gridHeaderRowCells.get(2)); + assertColumnHeader("Column 2", gridHeaderRowCells.get(3)); + + // when + dragAndDropDefaultColumnHeader(2, 0, 2); + + // then + gridHeaderRowCells = getGridHeaderRowCells(); + assertTrue(gridHeaderRowCells.get(0).getText().equals("")); + assertColumnHeader("Column 1", gridHeaderRowCells.get(1)); + assertColumnHeader("Column 0", gridHeaderRowCells.get(2)); + assertColumnHeader("Column 2", gridHeaderRowCells.get(3)); + } + @Test public void testColumnReordering_twoHeaderRows_dndReorderingPossibleFromFirstRow() { // given @@ -235,7 +309,6 @@ public class GridColumnReorderTest extends GridBasicFeaturesTest { selectMenuPath("Component", "Header", "Append row"); assertColumnHeaderOrder(0, 1, 2, 3); - // when // when dragAndDropColumnHeader(1, 0, 2, 100); -- cgit v1.2.3 From e70ac88c0f34c0d0ce9344da2891e466223632b5 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Fri, 27 Feb 2015 11:31:17 +0200 Subject: Escalator can now remove rows while spacers are open (#16644) Change-Id: I1fad1d14727d706bcc36021b77ec33dc8e76b632 --- .../src/com/vaadin/client/widgets/Escalator.java | 123 +++++++++++++++------ 1 file changed, 88 insertions(+), 35 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 0df078d26e..0f7ac891a9 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -2595,12 +2595,7 @@ public class Escalator extends Widget implements RequiresResize, return; } - final double pxDiff = numberOfRows * getDefaultRowHeight(); - for (SpacerContainer.SpacerImpl spacer : spacerContainer - .getSpacersForRowAndAfter(index)) { - spacer.setPositionDiff(0, pxDiff); - spacer.setRowIndex(spacer.getRow() + numberOfRows); - } + spacerContainer.shiftSpacersByRows(index, numberOfRows); /* * TODO: this method should probably only add physical rows, and not @@ -2679,7 +2674,6 @@ public class Escalator extends Widget implements RequiresResize, .getSpacerHeight(logicalRowIndexCursor++); final TableRowElement tr = i.next(); - getLogger().warning("iterate"); setRowPosition(tr, 0, rowTop); rowTop += getDefaultRowHeight(); } @@ -2895,13 +2889,9 @@ public class Escalator extends Widget implements RequiresResize, tr = visualRowOrder.get(i); } - getLogger().warning("y: " + y + ", index: " + i); setRowPosition(tr, 0, y); y += getDefaultRowHeight(); - double spacerHeight = spacerContainer.getSpacerHeight(i); - getLogger().warning( - "height: " + spacerHeight + ", index: " + i); - y += spacerHeight; + y += spacerContainer.getSpacerHeight(i); } return addedRows; @@ -2928,13 +2918,16 @@ public class Escalator extends Widget implements RequiresResize, return; } - // TODO - getLogger().warning("[[spacers]] removing rows"); - final Range viewportRange = getVisibleRowRange(); final Range removedRowsRange = Range .withLength(index, numberOfRows); + /* + * Removing spacers as the very first step will correct the + * scrollbars and row offsets right away. + */ + spacerContainer.paintRemoveSpacers(removedRowsRange); + final Range[] partitions = removedRowsRange .partitionWith(viewportRange); final Range removedAbove = partitions[0]; @@ -2960,7 +2953,7 @@ public class Escalator extends Widget implements RequiresResize, * 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 @@ -2995,7 +2988,7 @@ public class Escalator extends Widget implements RequiresResize, // 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 @@ -3026,9 +3019,12 @@ public class Escalator extends Widget implements RequiresResize, * 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); - setRowPosition(tr, 0, i * getDefaultRowHeight()); + setRowPosition(tr, 0, y); + y += getDefaultRowHeight(); + y += spacerContainer.getSpacerHeight(i); } /* @@ -3185,6 +3181,8 @@ public class Escalator extends Widget implements RequiresResize, final TableRowElement tr = visualRowOrder.get(i); setRowPosition(tr, 0, (int) newTop); newTop += getDefaultRowHeight(); + newTop += spacerContainer.getSpacerHeight(i + + removedLogicalInside.getStart()); } /* @@ -3283,13 +3281,15 @@ public class Escalator extends Widget implements RequiresResize, final ListIterator iterator = visualRowOrder .listIterator(removedVisualInside.getStart()); - 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); rowTop += getDefaultRowHeight(); + rowTop += spacerContainer.getSpacerHeight(i + + removedLogicalInside.getStart()); } } @@ -3309,14 +3309,18 @@ public class Escalator extends Widget implements RequiresResize, logicalTargetIndex); // move the surrounding rows to their correct places. + int firstUpdatedIndex = removedVisualInside.getEnd(); final ListIterator iterator = visualRowOrder - .listIterator(removedVisualInside.getEnd()); - 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); rowTop += getDefaultRowHeight(); + rowTop += spacerContainer.getSpacerHeight(firstUpdatedIndex + + i++); } } @@ -3625,6 +3629,9 @@ public class Escalator extends Widget implements RequiresResize, Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights"); + // TODO + getLogger().warning("[[spacer]] reapply default body row heights"); + /* step 1: resize and reposition rows */ for (int i = 0; i < visualRowOrder.size(); i++) { TableRowElement tr = visualRowOrder.get(i); @@ -3841,7 +3848,7 @@ public class Escalator extends Widget implements RequiresResize, * * @return the actual DOM count of escalator rows */ - private int getEscalatorRowCount() { + public int getEscalatorRowCount() { return root.getChildCount() - spacerContainer.getSpacersInDom().size(); } @@ -4495,6 +4502,10 @@ public class Escalator extends Widget implements RequiresResize, return positions.getTop(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); @@ -4528,6 +4539,34 @@ public class Escalator extends Widget implements RequiresResize, } } + public void paintRemoveSpacers(Range removedRowsRange) { + removeSpacers(removedRowsRange); + shiftSpacersByRows(removedRowsRange.getStart(), + -removedRowsRange.length()); + } + + @SuppressWarnings("boxing") + public void removeSpacers(Range removedRange) { + + Map removedSpacers = rowIndexToSpacer + .subMap(removedRange.getStart(), true, + removedRange.getEnd(), false); + + 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(); + } + + removedSpacers.clear(); + } + /** * Calculates the sum of all spacers. * @@ -4822,17 +4861,8 @@ public class Escalator extends Widget implements RequiresResize, return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); } - @SuppressWarnings("boxing") private void removeSpacer(int rowIndex) { - SpacerImpl spacer = getSpacer(rowIndex); - - // fix DOM - destroySpacerContent(spacer); - spacer.setHeight(0); // resets row offsets - spacer.getRootElement().removeFromParent(); - - // fix bookkeeping - rowIndexToSpacer.remove(rowIndex); + removeSpacers(Range.withOnly(rowIndex)); } public void setStylePrimaryName(String style) { @@ -4915,6 +4945,29 @@ public class Escalator extends Widget implements RequiresResize, spacer.setPosition(0, spacer.getTop() + diffPx); } } + + /** + * Shifts spacers at and after a specific row by an amount of rows. + *

+ * This moves both their associated row index and also their visual + * placement. + *

+ * Note: 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 class ElementPositionBookkeeper { @@ -5251,7 +5304,7 @@ public class Escalator extends Widget implements RequiresResize, * 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); -- cgit v1.2.3 From 1d5e5a9b1fdc23d0c61783eb5224cd0d826e881e Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 2 Mar 2015 11:55:35 +0200 Subject: Adds support for spacers near the bottom of Escalator (#16644) Change-Id: Idca22ecbe9a2db4dbfb26481694255c3a8791b67 --- client/src/com/vaadin/client/widgets/Escalator.java | 14 +++++++------- .../client/grid/EscalatorBasicClientFeaturesWidget.java | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 0f7ac891a9..b36b94c8f3 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -2421,13 +2421,6 @@ public class Escalator extends Widget implements RequiresResize, return; } - /* - * TODO This will break the logical index calculation, as it will - * try to search for non- - */ - getLogger().warning( - "[[spacers]] scrolling and spacers near the bottom"); - boolean rowsWereMoved = false; final double topElementPosition; @@ -2527,6 +2520,13 @@ public class Escalator extends Widget implements RequiresResize, 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); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index 37c0865e35..538f7a21a1 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -647,6 +647,7 @@ public class EscalatorBasicClientFeaturesWidget extends createSpacersMenuForRow(1, menupath); createSpacersMenuForRow(50, menupath); + createSpacersMenuForRow(99, menupath); } private void createSpacersMenuForRow(final int rowIndex, String[] menupath) { -- cgit v1.2.3 From 46be8e40721621e8946b2f2d64e6ecd1d9accdce Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 2 Mar 2015 15:56:17 +0200 Subject: Adds tests and JavaDoc regarding Escalator's spacers (#16644) Change-Id: I9b35a48df8b4e9801c26a25eb1ccce5351c61ddd --- .../client/widget/escalator/RowContainer.java | 26 +++++++++ .../src/com/vaadin/client/widgets/Escalator.java | 9 ++- .../EscalatorBasicClientFeaturesTest.java | 16 ++++++ .../escalator/EscalatorSpacerTest.java | 65 +++++++++++++++++++++- 4 files changed, 113 insertions(+), 3 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java index ea56c25062..e2baf9a03b 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -50,6 +50,10 @@ public interface RowContainer { *

* If a spacer is already registered with the given row index, that * spacer will be updated with the given height. + *

+ * Note: 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 @@ -59,6 +63,8 @@ public interface RowContainer { * 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; @@ -88,6 +94,26 @@ public interface RowContainer { */ SpacerUpdater getSpacerUpdater(); + /** + * {@inheritDoc} + *

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

+ * 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; } /** diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index b36b94c8f3..9d5526b14a 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -2925,6 +2925,12 @@ public class Escalator extends Widget implements RequiresResize, /* * 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); @@ -3576,7 +3582,7 @@ public class Escalator extends Widget implements RequiresResize, // TODO getLogger().warning( - "[[spacers]] removing rows while shrinking the body " + "[[spacers]] removing spacers while shrinking the body " + "section"); final ListIterator iter = visualRowOrder @@ -4511,6 +4517,7 @@ public class Escalator extends Widget implements RequiresResize, 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); } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index c22da47d62..25c4c1ff67 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -70,6 +70,7 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String COLSPAN_NONE = "Apply no colspan"; protected static final String SPACERS = "Spacers"; protected static final String ROW_1 = "Row 1"; + protected static final String ROW_99 = "Row 99"; protected static final String SET_100PX = "Set 100px"; protected static final String REMOVE = "Remove"; @@ -245,6 +246,11 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest getVerticalScrollbar().scroll(px); } + protected long getScrollTop() { + return ((Long) executeScript("return arguments[0].scrollTop;", + getVerticalScrollbar())).longValue(); + } + private TestBenchElement getVerticalScrollbar() { return (TestBenchElement) getEscalator().findElement( By.className("v-escalator-scroller-vertical")); @@ -254,6 +260,11 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest getHorizontalScrollbar().scrollLeft(px); } + private long getScrollLeft() { + return ((Long) executeScript("return arguments[0].scrollLeft;", + getHorizontalScrollbar())).longValue(); + } + private TestBenchElement getHorizontalScrollbar() { return (TestBenchElement) getEscalator().findElement( By.className("v-escalator-scroller-horizontal")); @@ -271,6 +282,11 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest return getEscalator().findElements(By.className("v-escalator-spacer")); } + protected boolean spacersAreFoundInDom() { + List spacers = getSpacers(); + return spacers != null && !spacers.isEmpty(); + } + @SuppressWarnings("boxing") protected WebElement getSpacer(int logicalRowIndex) { List spacers = getSpacers(); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index 1985de1c82..85acdc29cc 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -16,6 +16,7 @@ package com.vaadin.tests.components.grid.basicfeatures.escalator; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -28,6 +29,7 @@ import org.junit.Test; import org.openqa.selenium.WebElement; import com.vaadin.client.WidgetUtil; +import com.vaadin.testbench.elements.NotificationElement; import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; @SuppressWarnings("boxing") @@ -87,7 +89,8 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { @Test public void openVisibleSpacer() { - assertNull("No spacers should be shown at the start", getSpacer(1)); + assertFalse("No spacers should be shown at the start", + spacersAreFoundInDom()); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); assertNotNull("Spacer should be shown after setting it", getSpacer(1)); } @@ -117,7 +120,7 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); double oldTop = getElementTop(getSpacer(1)); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); - double newTop = getElementTop(getSpacer(1)); + double newTop = getElementTop(getSpacer(2)); assertGreater("Spacer should've been pushed down", newTop, oldTop); } @@ -152,6 +155,64 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { spacerTop + 100, rowTop, WidgetUtil.PIXEL_EPSILON); } + @Test + public void addSpacerAtBottomThenScrollThere() { + selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX); + scrollVerticallyTo(999999); + + assertFalse("Did not expect a notification", + $(NotificationElement.class).exists()); + } + + @Test + public void scrollToBottomThenAddSpacerThere() { + scrollVerticallyTo(999999); + long oldBottomScrollTop = getScrollTop(); + selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX); + + assertEquals("Adding a spacer underneath the current viewport should " + + "not scroll anywhere", oldBottomScrollTop, getScrollTop()); + assertFalse("Got an unexpected notification", + $(NotificationElement.class).exists()); + + scrollVerticallyTo(999999); + + assertFalse("Got an unexpected notification", + $(NotificationElement.class).exists()); + assertGreater("Adding a spacer should've made the scrollbar scroll " + + "further", getScrollTop(), oldBottomScrollTop); + } + + @Test + public void removingRowAboveSpacerMovesSpacerUp() { + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + WebElement spacer = getSpacer(1); + double originalElementTop = getElementTop(spacer); + + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, + REMOVE_ONE_ROW_FROM_BEGINNING); + assertLessThan("spacer should've moved up", getElementTop(spacer), + originalElementTop); + assertNull("No spacer for row 1 should be found after removing the " + + "top row", getSpacer(1)); + } + + @Test + public void removingSpacedRowRemovesSpacer() { + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + assertTrue("Spacer should've been found in the DOM", + spacersAreFoundInDom()); + + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, + REMOVE_ONE_ROW_FROM_BEGINNING); + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, + REMOVE_ONE_ROW_FROM_BEGINNING); + + assertFalse("No spacers should be in the DOM after removing " + + "associated spacer", spacersAreFoundInDom()); + + } + private static double getElementTop(WebElement element) { /* * we need to parse the style attribute, since using getCssValue gets a -- cgit v1.2.3 From b973c65eaff99ab2575854f12bb046e968baa3ff Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Tue, 3 Mar 2015 13:19:33 +0200 Subject: Escalator spacers are sized and placed like "fixed" (#16644) The width of a spacer is now 100% of the viewport (width of escalator minus possible scrollbars), and and are always horizontally frozen with the viewport. Change-Id: I6616747784cdb61551e144d941526dee815a7ef9 --- .../com/vaadin/client/widget/escalator/Spacer.java | 4 +- .../src/com/vaadin/client/widgets/Escalator.java | 115 +++++++++++++++++---- .../escalator/EscalatorSpacerTest.java | 111 +++++++++++++++----- 3 files changed, 184 insertions(+), 46 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/escalator/Spacer.java b/client/src/com/vaadin/client/widget/escalator/Spacer.java index 371d539f57..1e9985d6c1 100644 --- a/client/src/com/vaadin/client/widget/escalator/Spacer.java +++ b/client/src/com/vaadin/client/widget/escalator/Spacer.java @@ -16,10 +16,10 @@ package com.vaadin.client.widget.escalator; import com.google.gwt.dom.client.Element; -import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; /** - * A representation of a spacer element in a {@link BodyRowContainer}. + * A representation of a spacer element in a + * {@link com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer}. * * @since * @author Vaadin Ltd diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 9d5526b14a..1d7d9a9910 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -1698,13 +1698,11 @@ public class Escalator extends Widget implements RequiresResize, public void setColumnFrozen(int column, boolean frozen) { final NodeList childRows = root.getRows(); - if (column == 0 && this instanceof BodyRowContainer) { - // TODO - getLogger().warning("[[spacers]] freezing columns"); - } - 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) { @@ -1723,19 +1721,31 @@ public class Escalator extends Widget implements RequiresResize, public void updateFreezePosition(int column, double scrollLeft) { final NodeList childRows = root.getRows(); - if (column == 0 && this instanceof BodyRowContainer) { - // TODO - getLogger().warning("[[spacers]] update freeze position"); - } - 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. + *

+ * 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 true 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. @@ -2231,6 +2241,12 @@ public class Escalator extends Widget implements RequiresResize, 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; + } } private class HeaderRowContainer extends AbstractStaticRowContainer { @@ -3858,6 +3874,15 @@ public class Escalator extends Widget implements RequiresResize, return root.getChildCount() - spacerContainer.getSpacersInDom().size(); } + + @Override + protected boolean rowCanBeFrozen(TableRowElement tr) { + return visualRowOrder.contains(tr); + } + + public void reapplySpacerWidths() { + spacerContainer.reapplySpacerWidths(); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4376,18 +4401,13 @@ public class Escalator extends Widget implements RequiresResize, setPosition(getLeft() + x, getTop() + y); } - public double getLeft() { - // not implemented yet. - return 0; - } - 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); - root.getStyle().setWidth(100, Unit.PCT); spacerElement.getStyle().setWidth(100, Unit.PCT); spacerElement.setColSpan(getColumnConfiguration() @@ -4508,6 +4528,10 @@ public class Escalator extends Widget implements RequiresResize, 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}. @@ -4526,6 +4550,23 @@ public class Escalator extends Widget implements RequiresResize, 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; + public void setSpacer(int rowIndex, double height) throws IllegalArgumentException { @@ -4546,6 +4587,13 @@ public class Escalator extends Widget implements RequiresResize, } } + public void reapplySpacerWidths() { + for (SpacerImpl spacer : rowIndexToSpacer.values()) { + spacer.getRootElement().getStyle() + .setWidth(getInnerWidth(), Unit.PX); + } + } + public void paintRemoveSpacers(Range removedRowsRange) { removeSpacers(removedRowsRange); shiftSpacersByRows(removedRowsRange.getStart(), @@ -4559,6 +4607,10 @@ public class Escalator extends Widget implements RequiresResize, .subMap(removedRange.getStart(), true, removedRange.getEnd(), false); + if (removedSpacers.isEmpty()) { + return; + } + for (SpacerImpl spacer : removedSpacers.values()) { /* * [[optimization]] TODO: Each invocation of the setHeight @@ -4572,6 +4624,12 @@ public class Escalator extends Widget implements RequiresResize, } removedSpacers.clear(); + + if (rowIndexToSpacer.isEmpty()) { + assert spacerScrollerRegistration != null : "Spacer scroller registration was null"; + spacerScrollerRegistration.removeHandler(); + spacerScrollerRegistration = null; + } } /** @@ -4846,10 +4904,14 @@ public class Escalator extends Widget implements RequiresResize, @SuppressWarnings("boxing") private void insertNewSpacer(int rowIndex, double height) { + if (spacerScrollerRegistration == null) { + spacerScrollerRegistration = addScrollHandler(spacerScroller); + } + SpacerImpl spacer = new SpacerImpl(rowIndex); rowIndexToSpacer.put(rowIndex, spacer); - spacer.setPosition(0, calculateSpacerTop(rowIndex)); + spacer.setPosition(getScrollLeft(), calculateSpacerTop(rowIndex)); TableRowElement spacerRoot = spacer.getRootElement(); spacerRoot.getStyle().setWidth( @@ -4949,7 +5011,7 @@ public class Escalator extends Widget implements RequiresResize, double diffPx) { for (SpacerImpl spacer : rowIndexToSpacer.tailMap(changedRowIndex, false).values()) { - spacer.setPosition(0, spacer.getTop() + diffPx); + spacer.setPositionDiff(0, diffPx); } } @@ -4982,11 +5044,13 @@ public class Escalator extends Widget implements RequiresResize, * A map containing cached values of an element's current top position. */ private final Map elementTopPositionMap = new HashMap(); + private final Map elementLeftPositionMap = new HashMap(); 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) { @@ -4998,8 +5062,18 @@ public class Escalator extends Widget implements RequiresResize, 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); } } @@ -5621,6 +5695,7 @@ public class Escalator extends Widget implements RequiresResize, scroller.recalculateScrollbarsForVirtualViewport(); body.verifyEscalatorCount(); + body.reapplySpacerWidths(); Profiler.leave("Escalator.recalculateElementSizes"); } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index 85acdc29cc..5e56d9433a 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -44,7 +44,15 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { private final static String TRANSLATE_VALUE_REGEX = "translate(?:3d|)" // "translate" or "translate3d" + "\\(" // literal "(" - + ".+?, " // the x argument, uninteresting + + "(" // start capturing the x argument + + "[0-9]+" // the integer part of the value + + "(?:" // start of the subpixel part of the value + + "\\.[0-9]" // if we have a period, there must be at least one number after it + + "[0-9]*" // any amount of accuracy afterwards is fine + + ")?" // the subpixel part is optional + + ")" + + "(?:px)?" // we don't care if the values are suffixed by "px" or not. + + ", " + "(" // start capturing the y argument + "[0-9]+" // the integer part of the value + "(?:" // start of the subpixel part of the value @@ -52,14 +60,14 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { + "[0-9]*" // any amount of accuracy afterwards is fine + ")?" // the subpixel part is optional + ")" - + "(?:px)?" // we don't care if the values are suffixed by "px" or not. - + "(?:, .*?)?" // the possible z argument, uninteresting (translate doesn't have one, translate3d does) - + "\\)" // literal ")" - + ";?"; // optional ending semicolon + + "(?:px)?" // we don't care if the values are suffixed by "px" or not. + + "(?:, .*?)?" // the possible z argument, uninteresting (translate doesn't have one, translate3d does) + + "\\)" // literal ")" + + ";?"; // optional ending semicolon // 40px; // 12.34px - private final static String TOP_VALUE_REGEX = + private final static String PIXEL_VALUE_REGEX = "(" // capture the pixel value + "[0-9]+" // the pixel argument + "(?:" // start of the subpixel part of the value @@ -75,11 +83,13 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { .compile("transform: (.*?);"); // also matches "-webkit-transform"; private final static Pattern TOP_CSS_PATTERN = Pattern .compile("top: (.*?);"); + private final static Pattern LEFT_CSS_PATTERN = Pattern + .compile("left: (.*?);"); private final static Pattern TRANSLATE_VALUE_PATTERN = Pattern .compile(TRANSLATE_VALUE_REGEX); - private final static Pattern TOP_VALUE_PATTERN = Pattern - .compile(TOP_VALUE_REGEX); + private final static Pattern PIXEL_VALUE_PATTERN = Pattern + .compile(PIXEL_VALUE_REGEX); @Before public void before() { @@ -213,7 +223,33 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { } - private static double getElementTop(WebElement element) { + @Test + public void spacersAreFixedInViewport_firstFreezeThenScroll() { + selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN); + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + assertEquals("Spacer's left position should've been 0 at the " + + "beginning", 0d, getElementLeft(getSpacer(1)), + WidgetUtil.PIXEL_EPSILON); + + int scrollTo = 10; + scrollHorizontallyTo(scrollTo); + assertEquals("Spacer's left position should've been " + scrollTo + + " after scrolling " + scrollTo + "px", scrollTo, + getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON); + } + + @Test + public void spacersAreFixedInViewport_firstScrollThenFreeze() { + selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN); + int scrollTo = 10; + scrollHorizontallyTo(scrollTo); + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + assertEquals("Spacer's left position should've been " + scrollTo + + " after scrolling " + scrollTo + "px", scrollTo, + getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON); + } + + private static double[] getElementDimensions(WebElement element) { /* * we need to parse the style attribute, since using getCssValue gets a * normalized value that is harder to parse. @@ -222,17 +258,34 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { String transform = getTransformFromStyle(style); if (transform != null) { - return getTranslateYValue(transform); + return getTranslateValues(transform); } + double[] result = new double[] { -1, -1 }; + String left = getLeftFromStyle(style); + if (left != null) { + result[1] = getPixelValue(left); + } String top = getTopFromStyle(style); if (top != null) { - return getTopValue(top); + result[0] = getPixelValue(top); + } + + if (result[0] != -1 && result[1] != -1) { + return result; + } else { + throw new IllegalArgumentException( + "Could not parse the top position from the CSS \"" + style + + "\""); } + } - throw new IllegalArgumentException( - "Could not parse the top position from the CSS \"" + style - + "\""); + private static double getElementTop(WebElement element) { + return getElementDimensions(element)[1]; + } + + private static double getElementLeft(WebElement element) { + return getElementDimensions(element)[0]; } private static String getTransformFromStyle(String style) { @@ -243,6 +296,10 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { return getFromStyle(TOP_CSS_PATTERN, style); } + private static String getLeftFromStyle(String style) { + return getFromStyle(LEFT_CSS_PATTERN, style); + } + private static String getFromStyle(Pattern pattern, String style) { Matcher matcher = pattern.matcher(style); if (matcher.find()) { @@ -254,19 +311,25 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { } } - private static double getTranslateYValue(String translate) { - return getValueFromCss(TRANSLATE_VALUE_PATTERN, translate); - } + /** + * @return {@code [0] == x}, {@code [1] == y} + */ + private static double[] getTranslateValues(String translate) { + Matcher matcher = TRANSLATE_VALUE_PATTERN.matcher(translate); + assertTrue("no matches for " + translate + " against " + + TRANSLATE_VALUE_PATTERN, matcher.find()); + assertEquals("wrong amout of groups matched in " + translate, 2, + matcher.groupCount()); - private static double getTopValue(String top) { - return getValueFromCss(TOP_VALUE_PATTERN, top); + return new double[] { Double.parseDouble(matcher.group(1)), + Double.parseDouble(matcher.group(2)) }; } - private static double getValueFromCss(Pattern pattern, String css) { - Matcher matcher = pattern.matcher(css); - assertTrue("no matches for " + css + " against " - + TRANSLATE_VALUE_PATTERN, matcher.find()); - assertEquals("wrong amount of groups matched in " + css, 1, + private static double getPixelValue(String top) { + Matcher matcher = PIXEL_VALUE_PATTERN.matcher(top); + assertTrue("no matches for " + top + " against " + PIXEL_VALUE_PATTERN, + matcher.find()); + assertEquals("wrong amount of groups matched in " + top, 1, matcher.groupCount()); return Double.parseDouble(matcher.group(1)); } -- cgit v1.2.3 From b4a5adca6c2d69bb521ae71fd584bef7a320e7d0 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Tue, 3 Mar 2015 16:49:19 +0200 Subject: Adds support for -1 row index Escalator spacers (#16644) Change-Id: Iced2f089785983ce2ef1d2225517156cec4a7db4 --- .../vaadin/client/widget/escalator/RowContainer.java | 3 ++- client/src/com/vaadin/client/widgets/Escalator.java | 11 +++++++++-- .../EscalatorBasicClientFeaturesTest.java | 1 + .../basicfeatures/escalator/EscalatorSpacerTest.java | 20 +++++++++++++++++++- .../grid/EscalatorBasicClientFeaturesWidget.java | 1 + 5 files changed, 32 insertions(+), 4 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java index e2baf9a03b..b7aae1f4f3 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -57,7 +57,8 @@ public interface RowContainer { * * @param rowIndex * the row index for the spacer to modify. The affected - * spacer is underneath the given index + * 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 diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 1d7d9a9910..9b11e7aec1 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -4453,8 +4453,15 @@ public class Escalator extends Widget implements RequiresResize, .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(); + .getTopRowLogicalIndex() && !minusOneSpacerException; if (viewportNeedsScrolling) { /* @@ -4570,7 +4577,7 @@ public class Escalator extends Widget implements RequiresResize, public void setSpacer(int rowIndex, double height) throws IllegalArgumentException { - if (rowIndex < 0 || rowIndex >= getBody().getRowCount()) { + if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) { throw new IllegalArgumentException("invalid row index: " + rowIndex + ", while the body only has " + getBody().getRowCount() + " rows."); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index 25c4c1ff67..853489746f 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -69,6 +69,7 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String COLSPAN_NORMAL = "Apply normal colspan"; protected static final String COLSPAN_NONE = "Apply no colspan"; protected static final String SPACERS = "Spacers"; + protected static final String ROW_MINUS1 = "Row -1"; protected static final String ROW_1 = "Row 1"; protected static final String ROW_99 = "Row 99"; protected static final String SET_100PX = "Set 100px"; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index 5e56d9433a..da3472aebf 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -223,7 +223,7 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { } - @Test + @Test public void spacersAreFixedInViewport_firstFreezeThenScroll() { selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); @@ -249,6 +249,24 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON); } + @Test + public void addingMinusOneSpacerDoesNotScrollWhenScrolledAtTop() { + scrollVerticallyTo(5); + selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX); + assertEquals( + "No scroll adjustment should've happened when adding the -1 spacer", + 5, getScrollTop()); + } + + @Test + public void removingMinusOneSpacerScrolls() { + scrollVerticallyTo(5); + selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX); + selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, REMOVE); + assertEquals("Scroll adjustment should've happened when removing the " + + "-1 spacer", 0, getScrollTop()); + } + private static double[] getElementDimensions(WebElement element) { /* * we need to parse the style attribute, since using getCssValue gets a diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index 538f7a21a1..dc86b89167 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -645,6 +645,7 @@ public class EscalatorBasicClientFeaturesWidget extends } }, menupath); + createSpacersMenuForRow(-1, menupath); createSpacersMenuForRow(1, menupath); createSpacersMenuForRow(50, menupath); createSpacersMenuForRow(99, menupath); -- cgit v1.2.3 From de2172eda96ca3f34b2ab4e2e926b13957c113de Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Tue, 3 Mar 2015 11:15:06 +0200 Subject: Limit dnd-reorder of columns inside spanned headers. (#16643) Doesn't take footers into account, yet. Change-Id: I9c62ca97bcbdb852f2fe7ad7ea2e7de0f0ed02f8 --- .../vaadin/client/ui/dd/DragAndDropHandler.java | 23 ++-- client/src/com/vaadin/client/widgets/Grid.java | 138 +++++++++++++-------- .../grid/basicfeatures/GridBasicFeaturesTest.java | 11 ++ .../grid/basicfeatures/GridColumnReorderTest.java | 118 ++++++++++++++++++ 4 files changed, 232 insertions(+), 58 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java index eec52d5def..0710606818 100644 --- a/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -46,12 +46,15 @@ public class DragAndDropHandler { */ public interface DragAndDropCallback { /** - * Called when the drag has started. + * 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 */ - void onDragStart(NativeEvent startEvent); + boolean onDragStart(NativeEvent startEvent); /** * Called on drag. @@ -204,13 +207,15 @@ public class DragAndDropHandler { private void startDrag(NativeEvent startEvent, NativePreviewEvent triggerEvent, DragAndDropCallback callback) { - dragging = true; - // just capture something to prevent text selection in IE - Event.setCapture(RootPanel.getBodyElement()); - this.callback = callback; - dragHandlerRegistration = Event.addNativePreviewHandler(dragHandler); - callback.onDragStart(startEvent); - callback.onDragUpdate(triggerEvent); + 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() { diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index c237a7ba2f..26e7ba4650 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -3001,19 +3001,38 @@ public class Grid extends ResizeComposite implements } private void resolveDragElementHorizontalPosition(final int clientX) { - int left = clientX - table.getAbsoluteLeft(); - left = Math.max(0, Math.min(left, table.getClientWidth())); + 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 void onDragStart(NativeEvent startingEvent) { + 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); @@ -3026,12 +3045,11 @@ public class Grid extends ResizeComposite implements // mark the floating cell, for styling & testing dragElement.addClassName("dragged-column-header"); - calculatePossibleDropPositions(); - // start the auto scroll handler autoScroller.setScrollAreaPX(60); autoScroller.start(startingEvent, ScrollAxis.HORIZONTAL, autoScrollerCallback); + return true; } @Override @@ -3147,55 +3165,82 @@ public class Grid extends ResizeComposite implements } } + @SuppressWarnings("boxing") private void calculatePossibleDropPositions() { possibleDropPositions.clear(); - if (!calculatePossibleDropPositionInsideSpannedHeader()) { - HashMap columnIndexToDropPositionMap = new HashMap(); + final int draggedColumnIndex = eventCell.getColumnIndex(); + HashSet unavailableColumnDropIndices = new HashSet(); - final int frozenColumns = getSelectionAndFrozenColumnCount(); - double position = getFrozenColumnsWidth(); - // add all columns except frozen columns - for (int i = frozenColumns; i < getColumnCount(); i++) { - columnIndexToDropPositionMap.put(i, position); - position += getColumn(i).getWidthActual(); - } - // add the right side of the last column as columns.size() - columnIndexToDropPositionMap.put(getColumnCount(), position); + final int frozenColumns = getSelectionAndFrozenColumnCount(); + /* + * If the dragged cell is adjacent to 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; - // can't drop inside a spanned header from outside it - // -> remove all column indexes that are inside a spanned cell - // in any header row + for (int r = 0; r < getHeaderRowCount(); r++) { + HeaderRow headerRow = getHeaderRow(r); + if (!headerRow.hasSpannedCells()) { + continue; + } for (int c = frozenColumns; c < getColumnCount(); c++) { - for (int r = 0; r < getHeaderRowCount(); r++) { - HeaderRow headerRow = getHeaderRow(r); - if (headerRow.hasSpannedCells()) { - HeaderCell cell = headerRow.getCell(getColumn(c)); - assert cell != null : "Somehow got a null cell for row:cell " - + r + ":" + c; - int colspan = cell.getColspan(); - while (colspan > 1) { - c++; - colspan--; - columnIndexToDropPositionMap.remove(Integer - .valueOf(c)); - } + HeaderCell cell = headerRow.getCell(getColumn(c)); + assert cell != null : "Somehow got a null cell for row:cell " + + r + ":" + c; + int colspan = cell.getColspan(); + if (colspan <= 1) { + continue; + } + final int spannedCellRightEdgeIndex = c + colspan; + if (c <= draggedColumnIndex + && spannedCellRightEdgeIndex > draggedColumnIndex) { + // the spanned cell overlaps the dragged cell + if (c <= draggedColumnIndex && c > leftBound) { + leftBound = c; + } + if (spannedCellRightEdgeIndex < rightBound) { + rightBound = spannedCellRightEdgeIndex; + } + c = spannedCellRightEdgeIndex - 1; + } + + else { // can't drop inside a spanned cell + while (colspan > 1) { + c++; + colspan--; + unavailableColumnDropIndices.add(c); } } - } - // finally lets flip the map, because while dragging we want the - // column index matching the X-coordinate - for (Entry entry : columnIndexToDropPositionMap - .entrySet()) { - possibleDropPositions.put(entry.getValue(), entry.getKey()); } } - } - private boolean calculatePossibleDropPositionInsideSpannedHeader() { - // TODO if the dragged column is inside a spanned header on any row, - // then dragging is limited to inside that spanned cell - return false; + if (leftBound == (rightBound - 1)) { + return; + } + + double position = getFrozenColumnsWidth(); + // iterate column indices and add possible drop positions + for (int i = frozenColumns; i < getColumnCount(); i++) { + if (!unavailableColumnDropIndices.contains(i)) { + if (leftBound != -1) { + if (i >= leftBound && i <= rightBound) { + possibleDropPositions.put(position, i); + } + } else { + possibleDropPositions.put(position, i); + } + } + position += getColumn(i).getWidthActual(); + } + if (leftBound == -1) { + // add the right side of the last column as columns.size() + possibleDropPositions.put(position, getColumnCount()); + } } }; @@ -5494,12 +5539,7 @@ public class Grid extends ResizeComposite implements if (eventCell.getElement().getColSpan() > 1) { return false; } - // for now prevent dragging of columns belonging to a spanned cell - for (int r = 0; r < getHeaderRowCount(); r++) { - if (getHeaderRow(r).getCell(eventCell.getColumn()).getColspan() > 1) { - return false; - } - } + if (event.getTypeInt() == Event.ONMOUSEDOWN && event.getButton() == NativeEvent.BUTTON_LEFT || event.getTypeInt() == Event.ONTOUCHSTART) { diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java index 13f32fd2a2..0e4f0272dd 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java @@ -78,6 +78,11 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { getGridVerticalScrollbar()); } + protected void scrollGridHorizontallyTo(double px) { + executeScript("arguments[0].scrollLeft = " + px, + getGridHorizontalScrollbar()); + } + protected int getGridVerticalScrollPos() { return ((Number) executeScript("return arguments[0].scrollTop", getGridVerticalScrollbar())).intValue(); @@ -126,6 +131,12 @@ public abstract class GridBasicFeaturesTest extends MultiBrowserTest { By.xpath("//div[contains(@class, \"v-grid-scroller-vertical\")]")); } + protected WebElement getGridHorizontalScrollbar() { + return getDriver() + .findElement( + By.xpath("//div[contains(@class, \"v-grid-scroller-horizontal\")]")); + } + /** * Reloads the page without restartApplication. This occasionally breaks * stuff. diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java index 6e5eda43f1..39c6082bed 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -235,6 +235,124 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { assertColumnHeaderOrder(1, 2, 0, 3, 4); } + @Test + public void testColumnReorder_cellInsideASpannedHeader_cantBeDroppedOutsideSpannedArea() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(0, 2, 0, 20); + + // then + assertColumnHeaderOrder(0, 2, 1, 3, 4); + } + + @Test + public void testColumnReorder_cellInsideTwoCrossingSpanningHeaders_cantTouchThis() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + dragAndDropColumnHeader(0, 3, 0, 10); + assertColumnHeaderOrder(3, 0, 1, 2, 4); + + // when + dragAndDropColumnHeader(0, 2, 0, 10); + + // then + assertColumnHeaderOrder(3, 0, 1, 2, 4); + } + + @Test + public void testColumnReorder_cellsInsideSpannedHeaderAndBlockedByOtherSpannedCells_cantTouchThose() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + dragAndDropColumnHeader(0, 3, 0, 10); + assertColumnHeaderOrder(3, 0, 1, 2, 4); + + // when then + dragAndDropColumnHeader(0, 1, 3, 10); + assertColumnHeaderOrder(3, 0, 1, 2, 4); + + dragAndDropColumnHeader(1, 2, 1, 10); + assertColumnHeaderOrder(3, 0, 1, 2, 4); + + dragAndDropColumnHeader(2, 1, 3, -10); + assertColumnHeaderOrder(3, 0, 1, 2, 4); + } + + @Test + public void testColumnReorder_cellsInsideSpannedHeaderAndBlockedByOtherSpannedCells_reorderingLimited() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + dragAndDropColumnHeader(0, 0, 4, 100); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5); + + // when then + dragAndDropColumnHeader(0, 1, 4, 10); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5); + + dragAndDropColumnHeader(0, 2, 4, 10); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5); + + dragAndDropColumnHeader(0, 3, 4, 80); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 5, 4); + + dragAndDropColumnHeader(0, 4, 2, 100); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5); + + dragAndDropColumnHeader(2, 3, 4, 80); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 5, 4); + + dragAndDropColumnHeader(2, 4, 2, 100); + scrollGridHorizontallyTo(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5); + } + + @Test + public void testColumnReorder_cellsInsideTwoAdjacentSpannedHeaders_reorderingLimited() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + dragAndDropColumnHeader(0, 0, 4, 100); + scrollGridHorizontallyTo(0); + dragAndDropColumnHeader(0, 1, 4, 80); + scrollGridHorizontallyTo(0); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + assertColumnHeaderOrder(1, 3, 4, 5, 2); + + // when then + dragAndDropColumnHeader(0, 1, 3, 80); + assertColumnHeaderOrder(1, 4, 3, 5, 2); + + dragAndDropColumnHeader(0, 2, 4, 10); + assertColumnHeaderOrder(1, 4, 3, 5, 2); + + dragAndDropColumnHeader(0, 2, 0, 10); + assertColumnHeaderOrder(1, 3, 4, 5, 2); + } + private void toggleColumnReorder() { selectMenuPath("Component", "State", "Column Reordering"); } -- cgit v1.2.3 From 3abaa644d90c6460e6514b2e8207c178db01b52d Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Tue, 3 Mar 2015 14:26:44 +0200 Subject: Take footer into account in column dnd-reorder (#16643) Change-Id: I0358d1b18f65d40325b6c1bc3f3e4d3b9249fb5f --- client/src/com/vaadin/client/widgets/Grid.java | 14 ++-- .../grid/basicfeatures/GridColumnReorderTest.java | 75 +++++++++++++++++++++- 2 files changed, 82 insertions(+), 7 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 26e7ba4650..432228be1e 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -139,6 +139,8 @@ 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.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; @@ -3183,15 +3185,15 @@ public class Grid extends ResizeComposite implements int leftBound = -1; int rightBound = getColumnCount() + 1; - for (int r = 0; r < getHeaderRowCount(); r++) { - HeaderRow headerRow = getHeaderRow(r); - if (!headerRow.hasSpannedCells()) { + final List> rows = new ArrayList>(); + rows.addAll(header.getRows()); + rows.addAll(footer.getRows()); + for (StaticRow row : rows) { + if (!row.hasSpannedCells()) { continue; } for (int c = frozenColumns; c < getColumnCount(); c++) { - HeaderCell cell = headerRow.getCell(getColumn(c)); - assert cell != null : "Somehow got a null cell for row:cell " - + r + ":" + c; + StaticCell cell = row.getCell(getColumn(c)); int colspan = cell.getColspan(); if (colspan <= 1) { continue; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java index 39c6082bed..a7fc3f435d 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -236,7 +236,7 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { } @Test - public void testColumnReorder_cellInsideASpannedHeader_cantBeDroppedOutsideSpannedArea() { + public void testColumnReorder_cellInsideSpannedHeader_cantBeDroppedOutsideSpannedArea() { // given toggleColumnReorder(); selectMenuPath("Component", "Header", "Append row"); @@ -353,6 +353,79 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { assertColumnHeaderOrder(1, 3, 4, 5, 2); } + @Test + public void testColumnReorder_footerHasSpannedCells_cantDropInside() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Footer", "Append row"); + selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(0, 3, 1, 80); + + // then + assertColumnHeaderOrder(0, 3, 1, 2, 4); + } + + @Test + public void testColumnReorder_cellInsideASpannedFooter_cantBeDroppedOutsideSpannedArea() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Footer", "Append row"); + selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(0, 2, 0, 20); + + // then + assertColumnHeaderOrder(0, 2, 1, 3, 4); + } + + @Test + public void testColumnReorder_cellInsideTwoCrossingSpanningFooters_cantTouchThis() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Footer", "Append row"); + selectMenuPath("Component", "Footer", "Append row"); + selectMenuPath("Component", "Footer", "Row 1", "Join column cells 0, 1"); + selectMenuPath("Component", "Footer", "Row 2", "Join columns 1, 2"); + dragAndDropColumnHeader(0, 3, 0, 10); + assertColumnHeaderOrder(3, 0, 1, 2, 4); + + // when + dragAndDropColumnHeader(0, 2, 0, 10); + + // then + assertColumnHeaderOrder(3, 0, 1, 2, 4); + } + + @Test + public void testColumnReorder_cellsInsideTwoAdjacentSpannedHeaderAndFooter_reorderingLimited() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Footer", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + dragAndDropColumnHeader(0, 0, 4, 80); + scrollGridHorizontallyTo(0); + dragAndDropColumnHeader(0, 1, 4, 80); + scrollGridHorizontallyTo(0); + selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2"); + assertColumnHeaderOrder(1, 3, 4, 5, 2); + + // when then + dragAndDropColumnHeader(0, 1, 3, 80); + assertColumnHeaderOrder(1, 4, 3, 5, 2); + + dragAndDropColumnHeader(0, 2, 4, 10); + assertColumnHeaderOrder(1, 4, 3, 5, 2); + + dragAndDropColumnHeader(0, 2, 0, 10); + assertColumnHeaderOrder(1, 3, 4, 5, 2); + } + private void toggleColumnReorder() { selectMenuPath("Component", "State", "Column Reordering"); } -- cgit v1.2.3 From d6dbed15d626d0ae22cc711cfb08514be1c9a0db Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Wed, 4 Mar 2015 12:42:38 +0200 Subject: Fixes a bug with escalator.scrollToRow and spacers (#16644) Change-Id: I9e148cf81d393cc489c20bf0265f4a3bc6fed069 --- .../src/com/vaadin/client/widgets/Escalator.java | 6 ++--- server/src/com/vaadin/ui/Grid.java | 4 ++-- .../EscalatorBasicClientFeaturesTest.java | 26 +++++++++++++++------- .../escalator/EscalatorSpacerTest.java | 18 +++++++++++++++ .../grid/EscalatorBasicClientFeaturesWidget.java | 14 ++++++++++++ 5 files changed, 55 insertions(+), 13 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index efa777111a..679b452fde 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -1150,9 +1150,9 @@ public class Escalator extends Widget implements RequiresResize, public void scrollToRow(final int rowIndex, final ScrollDestination destination, final double padding) { - getLogger().warning("[[spacers]] scrollToRow"); - - final double targetStartPx = body.getDefaultRowHeight() * rowIndex; + final double targetStartPx = (body.getDefaultRowHeight() * rowIndex) + + body.spacerContainer + .getSpacerHeightsSumUntilIndex(rowIndex); final double targetEndPx = targetStartPx + body.getDefaultRowHeight(); diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index 6ab6c6b1a4..cf0e54156a 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -51,7 +51,6 @@ import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory; import com.vaadin.data.fieldgroup.FieldGroup; -import com.vaadin.data.fieldgroup.FieldGroup.BindException; import com.vaadin.data.fieldgroup.FieldGroup.CommitException; import com.vaadin.data.fieldgroup.FieldGroupFieldFactory; import com.vaadin.data.sort.Sort; @@ -2647,7 +2646,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * Getting a field before the editor has been opened depends on special * support from the {@link FieldGroup} in use. Using this method with a * user-provided FieldGroup might cause - * {@link BindException} to be thrown. + * {@link com.vaadin.data.fieldgroup.FieldGroup.BindException + * BindException} to be thrown. * * @return the bound field; or null if the respective * column is not editable diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index 61c75b6162..04c0933866 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -33,6 +33,10 @@ import com.vaadin.tests.tb3.MultiBrowserTest; @TestCategory("escalator") public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest { + + private static final String LOGICAL_ROW_ATTRIBUTE_NAME = "vLogicalRow"; + private static final String SPACER_CSS_CLASS = "v-escalator-spacer"; + protected static final String COLUMNS_AND_ROWS = "Columns and Rows"; protected static final String COLUMNS = "Columns"; @@ -52,6 +56,8 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String BODY_ROWS = "Body Rows"; protected static final String FOOTER_ROWS = "Footer Rows"; + protected static final String SCROLL_TO = "Scroll to..."; + protected static final String REMOVE_ALL_INSERT_SCROLL = "Remove all, insert 30 and scroll 40px"; protected static final String GENERAL = "General"; @@ -69,12 +75,15 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String COLUMN_SPANNING = "Column spanning"; protected static final String COLSPAN_NORMAL = "Apply normal colspan"; protected static final String COLSPAN_NONE = "Apply no colspan"; + protected static final String SET_100PX = "Set 100px"; protected static final String SPACERS = "Spacers"; + protected static final String REMOVE = "Remove"; + protected static final String ROW_MINUS1 = "Row -1"; protected static final String ROW_1 = "Row 1"; + protected static final String ROW_25 = "Row 25"; + protected static final String ROW_75 = "Row 75"; protected static final String ROW_99 = "Row 99"; - protected static final String SET_100PX = "Set 100px"; - protected static final String REMOVE = "Remove"; @Override protected Class getUIClass() { @@ -173,15 +182,16 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest private TestBenchElement getRow(String sectionTag, int row) { TestBenchElement escalator = getEscalator(); WebElement tableSection = escalator.findElement(By.tagName(sectionTag)); - By xpath; + String xpathExpression = "tr[not(@class='" + SPACER_CSS_CLASS + "')]"; if (row >= 0) { int fromFirst = row + 1; - xpath = By.xpath("tr[" + fromFirst + "]"); + xpathExpression += "[" + fromFirst + "]"; } else { int fromLast = Math.abs(row + 1); - xpath = By.xpath("tr[last() - " + fromLast + "]"); + xpathExpression += "[last() - " + fromLast + "]"; } + By xpath = By.xpath(xpathExpression); if (tableSection != null && ((TestBenchElement) tableSection).isElementPresent(xpath)) { return (TestBenchElement) tableSection.findElement(xpath); @@ -281,7 +291,7 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest } private List getSpacers() { - return getEscalator().findElements(By.className("v-escalator-spacer")); + return getEscalator().findElements(By.className(SPACER_CSS_CLASS)); } protected boolean spacersAreFoundInDom() { @@ -295,8 +305,8 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest System.out.println("size: " + spacers.size()); for (WebElement spacer : spacers) { System.out.println(spacer + ", " + logicalRowIndex); - Boolean isInDom = (Boolean) executeScript( - "return arguments[0]['vLogicalRow'] === arguments[1]", + Boolean isInDom = (Boolean) executeScript("return arguments[0]['" + + LOGICAL_ROW_ATTRIBUTE_NAME + "'] === arguments[1]", spacer, logicalRowIndex); if (isInDom) { return spacer; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java index da3472aebf..c3468b373e 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -267,6 +267,24 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { + "-1 spacer", 0, getScrollTop()); } + @Test + public void scrollToRowWorksProperlyWithSpacers() throws Exception { + selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX); + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + + /* + * we check for row -2 instead of -1, because escalator has the one row + * buffered underneath the footer + */ + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_75); + Thread.sleep(500); + assertEquals("Row 75: 0,75", getBodyCell(-2, 0).getText()); + + selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_25); + Thread.sleep(500); + assertEquals("Row 25: 0,25", getBodyCell(0, 0).getText()); + } + private static double[] getElementDimensions(WebElement element) { /* * we need to parse the style attribute, since using getCssValue gets a diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index dc86b89167..0d4aa305d9 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -16,6 +16,7 @@ import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widget.escalator.Spacer; import com.vaadin.client.widget.escalator.SpacerUpdater; import com.vaadin.client.widgets.Escalator; +import com.vaadin.shared.ui.grid.ScrollDestination; public class EscalatorBasicClientFeaturesWidget extends PureGWTTestApplication { @@ -571,6 +572,19 @@ public class EscalatorBasicClientFeaturesWidget extends escalator.setScrollTop(40); } }, menupath); + + String[] scrollToRowMenuPath = new String[menupath.length + 1]; + System.arraycopy(menupath, 0, scrollToRowMenuPath, 0, menupath.length); + scrollToRowMenuPath[scrollToRowMenuPath.length - 1] = "Scroll to..."; + for (int i = 0; i < 100; i += 25) { + final int rowIndex = i; + addMenuCommand("Row " + i, new ScheduledCommand() { + @Override + public void execute() { + escalator.scrollToRow(rowIndex, ScrollDestination.ANY, 0); + } + }, scrollToRowMenuPath); + } } private void createRowsMenu(final RowContainer container, String[] menupath) { -- cgit v1.2.3 From 3c84a54591d792b69a4d6d736714941edb1d818a Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Thu, 5 Mar 2015 11:10:06 +0200 Subject: Dnd-reorder of spanned cells in grid header. (#16643) Change-Id: I6f2decbbfb393922c4852f5b05ac0d10589ee512 --- client/src/com/vaadin/client/widgets/Grid.java | 101 +++++++++---- .../grid/basicfeatures/GridColumnReorderTest.java | 168 +++++++++++++++++++++ 2 files changed, 238 insertions(+), 31 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 432228be1e..07c888e936 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -3064,20 +3064,32 @@ public class Grid extends ResizeComposite implements @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 + 1)) { + && latestColumnDropIndex != (draggedColumnIndex + colspan)) { List> columns = getColumns(); - List> reordered = new ArrayList>( - columns); - Column moved = reordered.remove(draggedColumnIndex); + List> reordered = new ArrayList>(); if (draggedColumnIndex < latestColumnDropIndex) { - 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.add(latestColumnDropIndex, moved); reordered.remove(selectionColumn); // since setColumnOrder will // add it anyway! - @SuppressWarnings("unchecked") Column[] array = reordered.toArray(new Column[reordered .size()]); setColumnOrder(array); @@ -3096,13 +3108,15 @@ public class Grid extends ResizeComposite implements .findRowContainer(focusedCell.getElement()); if (focusedCellColumnIndex == draggedColumnIndex) { // move with the dragged column + final int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1 + : latestColumnDropIndex; cellFocusHandler.setCellFocus(focusedRowIndex, - latestColumnDropIndex, rowContainer); + adjustedDropIndex, rowContainer); } else if (latestColumnDropIndex <= focusedCellColumnIndex && draggedColumnIndex > focusedCellColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedCellColumnIndex + 1, rowContainer); - } else if (latestColumnDropIndex >= focusedCellColumnIndex + } else if (latestColumnDropIndex > focusedCellColumnIndex && draggedColumnIndex < focusedCellColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedCellColumnIndex - 1, rowContainer); @@ -3171,12 +3185,17 @@ public class Grid extends ResizeComposite implements private void calculatePossibleDropPositions() { possibleDropPositions.clear(); - final int draggedColumnIndex = eventCell.getColumnIndex(); - HashSet unavailableColumnDropIndices = new HashSet(); - + final int draggedCellIndex = eventCell.getColumnIndex(); + final StaticRow draggedCellRow = header.getRow(eventCell + .getRowIndex()); + final int draggedCellRightIndex = draggedCellIndex + + draggedCellRow.getCell(eventCell.getColumn()) + .getColspan(); final int frozenColumns = getSelectionAndFrozenColumnCount(); + final Range draggedCellRange = Range.between(draggedCellIndex, + draggedCellRightIndex); /* - * If the dragged cell is adjacent to a spanned cell in any other + * 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 @@ -3185,6 +3204,7 @@ public class Grid extends ResizeComposite implements int leftBound = -1; int rightBound = getColumnCount() + 1; + final HashSet unavailableColumnDropIndices = new HashSet(); final List> rows = new ArrayList>(); rows.addAll(header.getRows()); rows.addAll(footer.getRows()); @@ -3192,30 +3212,52 @@ public class Grid extends ResizeComposite implements if (!row.hasSpannedCells()) { continue; } - for (int c = frozenColumns; c < getColumnCount(); c++) { - StaticCell cell = row.getCell(getColumn(c)); + 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 spannedCellRightEdgeIndex = c + colspan; - if (c <= draggedColumnIndex - && spannedCellRightEdgeIndex > draggedColumnIndex) { - // the spanned cell overlaps the dragged cell - if (c <= draggedColumnIndex && c > leftBound) { - leftBound = c; + 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 <= draggedCellIndex + && cellColumnIndex > leftBound) { + leftBound = cellColumnIndex; } - if (spannedCellRightEdgeIndex < rightBound) { - rightBound = spannedCellRightEdgeIndex; + if (cellColumnRightIndex < rightBound) { + rightBound = cellColumnRightIndex; } - c = spannedCellRightEdgeIndex - 1; + cellColumnIndex = cellColumnRightIndex - 1; } - else { // can't drop inside a spanned cell + else { // can't drop inside a spanned cell, or this is the + // dragged cell while (colspan > 1) { - c++; + cellColumnIndex++; colspan--; - unavailableColumnDropIndices.add(c); + unavailableColumnDropIndices.add(cellColumnIndex); } } } @@ -3244,6 +3286,7 @@ public class Grid extends ResizeComposite implements possibleDropPositions.put(position, getColumnCount()); } } + }; /** @@ -5537,10 +5580,6 @@ public class Grid extends ResizeComposite implements .getFrozenColumnCount()) { return false; } - // for now prevent dragging of spanned cells - if (eventCell.getElement().getColSpan() > 1) { - return false; - } if (event.getTypeInt() == Event.ONMOUSEDOWN && event.getButton() == NativeEvent.BUTTON_LEFT diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java index a7fc3f435d..d4892fb52a 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnReorderTest.java @@ -426,6 +426,174 @@ public class GridColumnReorderTest extends GridBasicClientFeaturesTest { assertColumnHeaderOrder(1, 3, 4, 5, 2); } + @Test + public void testColumnReorder_draggingASpannedCell_dragWorksNormally() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(1, 1, 4, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 3, 1, 2, 4); + } + + @Test + public void testColumnReorder_twoEqualSpannedCells_bothCanBeDragged() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(1, 1, 4, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 3, 1, 2, 4); + + // when + dragAndDropColumnHeader(2, 3, 0, 10); + + // then + assertColumnHeaderOrder(1, 2, 0, 3, 4); + } + + @Test + public void testColumReorder_twoCrossingSpanningHeaders_neitherCanBeDragged() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 3", "Join column cells 0, 1"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(1, 1, 4, 10); + + // then + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(2, 0, 3, 100); + + // then + assertColumnHeaderOrder(0, 1, 2, 3, 4); + } + + @Test + public void testColumnReorder_spannedCellHasAnotherSpannedCellInside_canBeDraggedNormally() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + dragAndDropColumnHeader(1, 3, 1, 10); + scrollGridHorizontallyTo(0); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 3, 4, 5, 1); + + // when + dragAndDropColumnHeader(1, 1, 0, 10); + + // then + assertColumnHeaderOrder(3, 4, 5, 0, 1); + } + + @Test + public void testColumnReorder_spannedCellInsideAnotherSpanned_canBeDraggedWithBoundaries() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + dragAndDropColumnHeader(1, 3, 1, 10); + scrollGridHorizontallyTo(0); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 3, 4, 5, 1); + + // when + dragAndDropColumnHeader(2, 1, 3, 100); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 5, 3, 4, 1); + + // when + dragAndDropColumnHeader(2, 2, 0, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 3, 4, 5, 1); + } + + @Test + public void testColumnReorder_cellInsideAndNextToSpannedCells_canBeDraggedWithBoundaries() { + // given + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + dragAndDropColumnHeader(1, 3, 1, 10); + scrollGridHorizontallyTo(0); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 3, 4, 5, 1); + + // when + dragAndDropColumnHeader(2, 3, 0, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 5, 3, 4, 1); + + // when + dragAndDropColumnHeader(2, 1, 4, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 3, 4, 5, 1); + } + + @Test + public void testColumnReorder_multipleSpannedCells_dragWorksNormally() { + toggleColumnReorder(); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5"); + selectMenuPath("Component", "Header", "Append row"); + selectMenuPath("Component", "Header", "Row 3", "Join columns 1, 2"); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(1, 3, 2, -10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 3, 4, 5, 1); + + // when + scrollGridHorizontallyTo(100); + dragAndDropColumnHeader(2, 4, 2, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + // when + dragAndDropColumnHeader(0, 0, 3, 10); + scrollGridHorizontallyTo(0); + + // then + assertColumnHeaderOrder(1, 2, 0, 3, 4); + } + private void toggleColumnReorder() { selectMenuPath("Component", "State", "Column Reordering"); } -- cgit v1.2.3 From f31a927acb4168497f6f25ab68120dd9fa03e636 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Thu, 5 Mar 2015 11:09:26 +0200 Subject: Double-opening Grid details row doesn't change height (#16644) The bug caused an arbitrary height to be set for the details row if an already-open details was reopened. Change-Id: I1d764511d2829416491eb92abf8565288252f953 --- client/src/com/vaadin/client/widgets/Grid.java | 58 ++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 2725f84fe6..aa768f9f21 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -1782,6 +1782,12 @@ public class Grid 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 eventCell = new EventCellReference(this); @@ -2929,8 +2935,9 @@ public class Grid 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 visibleDetails = new HashSet(); /** * Enumeration for easy setting of selection mode. @@ -6341,17 +6348,60 @@ public class Grid extends ResizeComposite implements /** * Shows or hides the details for a specific row. + *

+ * This method does nothing if trying to set show already-visible details, + * or hide already-hidden details. * * @since - * @param row + * @param rowIndex * the index of the affected row * @param visible * true to show the details, or false * to hide them + * @see #isDetailsVisible(int) */ public void setDetailsVisible(int rowIndex, boolean visible) { - double height = visible ? DETAILS_ROW_INITIAL_HEIGHT : -1; - escalator.getBody().setSpacer(rowIndex, height); + 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. + */ + + if (visible && !isDetailsVisible(rowIndex)) { + escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT); + visibleDetails.add(rowIndexInteger); + } + + else if (!visible && isDetailsVisible(rowIndex)) { + escalator.getBody().setSpacer(rowIndex, -1); + visibleDetails.remove(rowIndexInteger); + } + } + + /** + * Check whether the details for a row is visible or not. + * + * @since + * @param rowIndex + * the index of the row for which to check details + * @return true iff the details for the given row is visible + * @see #setDetailsVisible(int, boolean) + */ + public boolean isDetailsVisible(int rowIndex) { + return visibleDetails.contains(Integer.valueOf(rowIndex)); } /** -- cgit v1.2.3 From f091f9ff68e1d3b1c06c8e78b33cd07ccf480f9f Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Thu, 12 Mar 2015 11:19:43 +0200 Subject: Hide columns with client side API in Grid (#17023) For now, the width for spanned header/footer cells is broken after hiding their columns. Change-Id: I0833fb570fcad731776a88a183b3df45cbc19557 --- client/src/com/vaadin/client/widgets/Grid.java | 131 +++++++++++++++------ .../grid/basicfeatures/GridColumnHidingTest.java | 88 ++++++++++++++ .../client/grid/GridBasicClientFeaturesWidget.java | 6 + 3 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 7b336ff5ee..c7ec9cd062 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -2477,7 +2477,7 @@ public class Grid 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) { @@ -2493,7 +2493,7 @@ public class Grid extends ResizeComposite implements /* Step 1: Apply all column widths as they are. */ Map selfWidths = new LinkedHashMap(); - List> columns = getColumns(); + List> columns = getVisibleColumns(); for (int index = 0; index < columns.size(); index++) { selfWidths.put(index, columns.get(index).getWidth()); } @@ -2534,6 +2534,7 @@ public class Grid extends ResizeComposite implements final Set> columnsToExpand = new HashSet>(); List> nonFixedColumns = new ArrayList>(); Map columnSizes = new HashMap(); + final List> visibleColumns = getVisibleColumns(); /* * Set all fixed widths and also calculate the size-to-fit widths @@ -2542,7 +2543,7 @@ public class Grid extends ResizeComposite implements * This way we know with how many pixels we have left to expand the * rest. */ - for (Column column : getColumns()) { + for (Column column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; final double widthFixed = Math.max(widthAsIs, @@ -2551,11 +2552,11 @@ public class Grid extends ResizeComposite implements && column.getExpandRatio() == -1; 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); } } @@ -2572,7 +2573,7 @@ public class Grid extends ResizeComposite implements columnsToExpand.add(column); } reservedPixels += newWidth; - columnSizes.put(indexOfColumn(column), newWidth); + columnSizes.put(visibleColumns.indexOf(column), newWidth); } /* @@ -2600,8 +2601,8 @@ public class Grid extends ResizeComposite implements final Column 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; @@ -2611,7 +2612,7 @@ public class Grid extends ResizeComposite implements totalRatios -= expandRatio; aColumnHasMaxedOut = true; pixelsToDistribute -= maxWidth - autoWidth; - columnSizes.put(indexOfColumn(column), maxWidth); + columnSizes.put(columnIndex, maxWidth); } } } while (aColumnHasMaxedOut); @@ -2647,13 +2648,14 @@ public class Grid extends ResizeComposite implements for (Column 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; } @@ -2674,7 +2676,7 @@ public class Grid extends ResizeComposite implements * remove those pixels from other columns */ double pixelsToRemoveFromOtherColumns = 0; - for (Column column : getColumns()) { + for (Column column : visibleColumns) { /* * We can't iterate over columnsToExpand, even though that * would be convenient. This is because some column without @@ -2683,11 +2685,11 @@ public class Grid 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; @@ -2713,7 +2715,7 @@ public class Grid extends ResizeComposite implements for (Column 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); } @@ -3379,6 +3381,8 @@ public class Grid extends ResizeComposite implements private boolean editable = true; + private boolean hidden = false; + private String headerCaption = ""; private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; @@ -3549,6 +3553,9 @@ public class Grid 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. + *

+ * 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 @@ -3556,14 +3563,17 @@ public class Grid extends ResizeComposite implements public Column 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); @@ -3575,6 +3585,9 @@ public class Grid extends ResizeComposite implements *

* Note: If a negative value was given to * {@link #setWidth(double)}, that same negative value is returned here. + *

+ * Note: 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. @@ -3589,13 +3602,18 @@ public class Grid extends ResizeComposite implements * Returns the effective pixel width of the column. *

* 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() { @@ -3632,6 +3650,41 @@ public class Grid extends ResizeComposite implements return sortable; } + /** + * Hides or shows the column. By default columns are visible before + * explicitly hiding them. + * + * @since + * @param hidden + * true to hide the column, false + * to show + */ + public void setHidden(boolean hidden) { + if (this.hidden != hidden) { + if (hidden) { + grid.escalator.getColumnConfiguration().removeColumns( + grid.getVisibleColumns().indexOf(this), 1); + this.hidden = hidden; + } else { + this.hidden = hidden; + grid.escalator.getColumnConfiguration().insertColumns( + grid.getVisibleColumns().indexOf(this), 1); + } + scheduleColumnWidthRecalculator(); + } + } + + /** + * Is this column hidden. Default is {@code false}. + * + * @since + * @return true if the column is currently hidden, + * false otherwise + */ + public boolean isHidden() { + return hidden; + } + @Override public String toString() { String details = ""; @@ -4577,7 +4630,8 @@ public class Grid 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(); @@ -4592,6 +4646,8 @@ public class Grid extends ResizeComposite implements /** * Returns the amount of columns in the grid. + *

+ * NOTE: this includes the hidden columns in the count. * * @return The number of columns in the grid */ @@ -4600,7 +4656,9 @@ public class Grid extends ResizeComposite implements } /** - * Returns a list of columns in the grid. + * Returns a list columns in the grid, including hidden columns. + *

+ * For currently visible columns, use {@link #getVisibleColumns()}. * * @return A unmodifiable list of the columns in the grid */ @@ -4609,6 +4667,24 @@ public class Grid extends ResizeComposite implements .unmodifiableList(new ArrayList>(columns)); } + /** + * Returns a list of the currently visible columns in the grid. + *

+ * No {@link Column#isHidden() hidden} columns included. + * + * @since + * @return A unmodifiable list of the currently visible columns in the grid + */ + public List> getVisibleColumns() { + ArrayList> visible = new ArrayList>(); + for (Column c : columns) { + if (!c.isHidden()) { + visible.add(c); + } + } + return Collections.unmodifiableList(visible); + } + /** * Returns a column by its index in the grid. * @@ -4625,17 +4701,6 @@ public class Grid extends ResizeComposite implements return columns.get(index); } - /** - * Returns current index of given column - * - * @param column - * column in grid - * @return column index, or -1 if not in this Grid - */ - protected int indexOfColumn(Column column) { - return columns.indexOf(column); - } - /** * Returns the header section of this grid. The default header contains a * single row displaying the column captions. diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java new file mode 100644 index 0000000000..b4593dac28 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.testbench.parallel.TestCategory; + +@TestCategory("grid") +public class GridColumnHidingTest extends GridBasicClientFeaturesTest { + + @Before + public void before() { + openTestURL(); + } + + @Test + public void testColumnHiding_hidingColumnsFromAPI_works() { + selectMenuPath("Component", "State", "Width", "1000px"); + assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); + + toggleHideColumn(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5, 6); + + toggleHideColumn(1); + toggleHideColumn(2); + toggleHideColumn(3); + assertColumnHeaderOrder(4, 5, 6, 7, 8); + } + + @Test + public void testColumnHiding_unhidingColumnsFromAPI_works() { + selectMenuPath("Component", "State", "Width", "1000px"); + assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); + + toggleHideColumn(0); + assertColumnHeaderOrder(1, 2, 3, 4, 5, 6); + + toggleHideColumn(0); + assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); + + toggleHideColumn(1); + toggleHideColumn(2); + toggleHideColumn(3); + assertColumnHeaderOrder(0, 4, 5, 6, 7, 8); + + toggleHideColumn(1); + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 2, 4, 5, 6); + } + + @Test + public void testColumnHiding_hidingUnhidingFromAPI_works() { + selectMenuPath("Component", "State", "Width", "1000px"); + assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); + + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 3, 4, 5, 6); + + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); + + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 3, 4, 5, 6); + + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); + } + + private void toggleHideColumn(int columnIndex) { + selectMenuPath("Component", "Columns", "Column " + columnIndex, + "Hidden"); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index 9e77438e36..5ec8058ae9 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -734,6 +734,12 @@ public class GridBasicClientFeaturesWidget extends column.setSortable(!column.isSortable()); } }, "Component", "Columns", "Column " + i); + addMenuCommand("Hidden", new ScheduledCommand() { + @Override + public void execute() { + column.setHidden(!column.isHidden()); + } + }, "Component", "Columns", "Column " + i); addMenuCommand("auto", new ScheduledCommand() { @Override -- cgit v1.2.3 From 54d6480a3cbd6d721dc119a7f6c5c029b7dcefea Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Fri, 13 Mar 2015 12:20:30 +0200 Subject: Client side event for grid's columns visibility change (#17023) Change-Id: I1965ca6c298366d89b1940a992788d042cf7a4aa --- .../widget/grid/events/ColumnReorderEvent.java | 3 +- .../grid/events/ColumnVisibilityChangeEvent.java | 93 ++++++++++++++++++++++ .../grid/events/ColumnVisibilityChangeHandler.java | 39 +++++++++ client/src/com/vaadin/client/widgets/Grid.java | 18 +++++ .../grid/basicfeatures/GridColumnHidingTest.java | 46 +++++++++++ .../client/grid/GridBasicClientFeaturesWidget.java | 42 ++++++++++ 6 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java create mode 100644 client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java (limited to 'client') diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java index 4890ed063d..c72da0c48e 100644 --- a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java @@ -18,8 +18,7 @@ package com.vaadin.client.widget.grid.events; import com.google.gwt.event.shared.GwtEvent; /** - * An event for notifying that the columns in the Grid's columns have been - * reordered. + * An event for notifying that the columns in the Grid have been reordered. * * @param * The row type of the grid. The row type is the POJO type from where 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..10bfbfad68 --- /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 + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public class ColumnVisibilityChangeEvent extends + GwtEvent> { + + private final static Type> TYPE = new Type>(); + + public static final Type> getType() { + return TYPE; + } + + private final Column column; + + private final boolean userOriginated; + + private final boolean hidden; + + public ColumnVisibilityChangeEvent(Column 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 getColumn() { + return column; + } + + /** + * Is the column hidden or visible. + * + * @return true if the column was hidden false if + * it was set visible + */ + public boolean isHidden() { + return hidden; + } + + /** + * Is the visibility change triggered by user. + * + * @return true if the change was triggered by user, + * false if not + */ + public boolean isUserOriginated() { + return userOriginated; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public com.google.gwt.event.shared.GwtEvent.Type> getAssociatedType() { + return (Type) TYPE; + } + + @Override + protected void dispatch(ColumnVisibilityChangeHandler 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..10a7660954 --- /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 The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public interface ColumnVisibilityChangeHandler 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 event); +} diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index c7ec9cd062..7e08348da0 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -107,6 +107,8 @@ 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; @@ -3671,6 +3673,8 @@ public class Grid extends ResizeComposite implements grid.getVisibleColumns().indexOf(this), 1); } scheduleColumnWidthRecalculator(); + this.grid.fireEvent(new ColumnVisibilityChangeEvent(this, + hidden, false)); } } @@ -6417,6 +6421,20 @@ public class Grid extends ResizeComposite implements 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 + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnVisibilityChangeHandler( + ColumnVisibilityChangeHandler handler) { + return addHandler(handler, ColumnVisibilityChangeEvent.getType()); + } + /** * Apply sorting to data source. */ diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java index b4593dac28..6d38c25fb6 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridColumnHidingTest.java @@ -15,8 +15,13 @@ */ package com.vaadin.tests.components.grid.basicfeatures; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + import org.junit.Before; import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; import com.vaadin.testbench.parallel.TestCategory; @@ -81,6 +86,47 @@ public class GridColumnHidingTest extends GridBasicClientFeaturesTest { assertColumnHeaderOrder(0, 1, 2, 3, 4, 5, 6); } + @Test + public void testColumnHiding_onVisibilityChange_triggersClientSideEvent() { + assertColumnHeaderOrder(0, 1, 2, 3, 4); + selectMenuPath("Component", "Internals", "Listeners", + "Add Column Visibility Change listener"); + + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 3, 4); + + WebElement webElement = findElement(By.id("columnvisibility")); + int counter = Integer.parseInt(webElement.getAttribute("counter")); + int columnIndex = Integer.parseInt(webElement + .getAttribute("columnindex")); + boolean userOriginated = Boolean.parseBoolean(webElement + .getAttribute("useroriginated")); + boolean hidden = Boolean.parseBoolean(webElement + .getAttribute("ishidden")); + + assertNotNull("no event fired", webElement); + assertEquals(1, counter); + assertEquals(2, columnIndex); + assertEquals(false, userOriginated); + assertEquals(true, hidden); + + toggleHideColumn(2); + assertColumnHeaderOrder(0, 1, 2, 3, 4); + + webElement = findElement(By.id("columnvisibility")); + counter = Integer.parseInt(webElement.getAttribute("counter")); + columnIndex = Integer.parseInt(webElement.getAttribute("columnIndex")); + userOriginated = Boolean.parseBoolean(webElement + .getAttribute("userOriginated")); + hidden = Boolean.parseBoolean(webElement.getAttribute("ishidden")); + + assertNotNull("no event fired", webElement); + assertEquals(2, counter); + assertEquals(2, columnIndex); + assertEquals(false, userOriginated); + assertEquals(false, hidden); + } + private void toggleHideColumn(int columnIndex) { selectMenuPath("Component", "Columns", "Column " + columnIndex, "Hidden"); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java index 5ec8058ae9..9131a2bdbe 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java @@ -57,6 +57,8 @@ import com.vaadin.client.widget.grid.events.BodyKeyPressHandler; import com.vaadin.client.widget.grid.events.BodyKeyUpHandler; import com.vaadin.client.widget.grid.events.ColumnReorderEvent; import com.vaadin.client.widget.grid.events.ColumnReorderHandler; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; import com.vaadin.client.widget.grid.events.FooterKeyPressHandler; import com.vaadin.client.widget.grid.events.FooterKeyUpHandler; @@ -471,6 +473,46 @@ public class GridBasicClientFeaturesWidget extends }); } }, listenersPath); + addMenuCommand("Add Column Visibility Change listener", + new ScheduledCommand() { + private HandlerRegistration columnVisibilityHandler = null; + + @Override + public void execute() { + if (columnVisibilityHandler != null) { + return; + } + final Label columnOrderLabel = new Label(); + columnOrderLabel.getElement().setId("columnvisibility"); + addLineEnd(columnOrderLabel, 250); + ColumnVisibilityChangeHandler handler = new ColumnVisibilityChangeHandler>() { + + private int eventIndex = 0; + + @Override + public void onVisibilityChange( + ColumnVisibilityChangeEvent> event) { + columnOrderLabel.getElement().setAttribute( + "counter", "" + (++eventIndex)); + columnOrderLabel.getElement().setAttribute( + "useroriginated", + (Boolean.toString(event + .isUserOriginated()))); + columnOrderLabel.getElement().setAttribute( + "ishidden", + (Boolean.toString(event.isHidden()))); + columnOrderLabel.getElement().setAttribute( + "columnindex", + "" + + grid.getColumns().indexOf( + event.getColumn())); + } + }; + + columnVisibilityHandler = grid + .addColumnVisibilityChangeHandler(handler); + } + }, listenersPath); } private void createStateMenu() { -- cgit v1.2.3 From 8ece5b005a8edb5c50d06c0ac9758044f7e8b877 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Fri, 13 Mar 2015 14:39:23 +0200 Subject: Client side API for setting a grid column hideable (#17023) Change-Id: I389d603210744ea02846a00ab893188b7953145f --- client/src/com/vaadin/client/widgets/Grid.java | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'client') diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index 7e08348da0..f7744d52ae 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -3385,6 +3385,8 @@ public class Grid extends ResizeComposite implements private boolean hidden = false; + private boolean hideable = false; + private String headerCaption = ""; private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; @@ -3689,6 +3691,38 @@ public class Grid extends ResizeComposite implements return hidden; } + /** + * Set whether it is possible for the user to hide this column or not. + * Default is {@code false}. + *

+ * Note: it is still possible to hide the column + * programmatically using {@link #setHidden(boolean)}. + * + * @since + * @param hideable + * true if the user can hide this column, + * false if not + */ + public void setHideable(boolean hideable) { + this.hideable = hideable; + // TODO update whether sidebar/popup can be opened + } + + /** + * Is it possible for the the user to hide this column. Default is + * {@code false}. + *

+ * Note: the column can be programmatically hidden using + * {@link #setHidden(boolean)} regardless of the returned value. + * + * @since + * @return true if the user can hide the column, + * false if not + */ + public boolean isHideable() { + return hideable; + } + @Override public String toString() { String details = ""; -- cgit v1.2.3